<template>
  <div class="recipe-manager">
    <!-- Login prompt for non-logged-in users -->
    <div v-if="!auth.isLoggedIn" class="login-prompt">
      <p>登录后可管理配方、创建个人配方集</p>
      <button class="btn-primary" @click="ui.openLogin()">登录 / 注册</button>
    </div>
    <template v-else>
    <!-- Review Bar (admin + senior_editor with assigned reviews) -->
    <div v-if="(auth.isAdmin || auth.canManage) && pendingCount > 0" class="review-bar" @click="showPending = !showPending" >
      📝 待审核配方: {{ pendingCount }} 条
      <span class="toggle-icon">{{ showPending ? '▾' : '▸' }}</span>
    </div>
    <div v-if="showPending && pendingRecipes.length" class="pending-list">
      <div v-for="r in pendingRecipes" :key="r._id" class="pending-item">
        <span class="pending-name clickable" @click="openRecipeDetail(r)">{{ r.name }}</span>
        <span class="pending-owner">{{ r._owner_name }}</span>
        <template v-if="auth.isAdmin">
          <button class="btn-sm btn-approve" @click="approveRecipe(r)">通过</button>
          <button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
          <button class="btn-sm btn-outline" @click="r._showAssign = !r._showAssign">指派</button>
          <div v-if="r._showAssign" class="assign-row">
            <select v-model="r._assignTo" class="assign-select">
              <option value="">选择审核人...</option>
              <option v-for="u in seniorEditors" :key="u.id" :value="u.id">{{ u.display_name || u.username }}</option>
            </select>
            <button class="btn-sm btn-primary" @click="assignReview(r)" :disabled="!r._assignTo">发送</button>
          </div>
        </template>
        <template v-else>
          <button class="btn-sm btn-approve" @click="recommendApprove(r)">推荐通过</button>
          <button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
        </template>
      </div>
    </div>

    <!-- Search bar -->
    <div class="search-bar">
      <div class="search-box">
        <input class="search-input" v-model="manageSearch" placeholder="搜索配方名、精油、标签..." />
        <button v-if="manageSearch" class="search-clear-btn" @click="manageSearch = ''">✕</button>
      </div>
    </div>

    <!-- Action buttons -->
    <div class="action-bar">
      <button class="action-chip" @click="showAddOverlay = true">新增</button>
      <button class="action-chip" :class="{ active: isAllSelected }" @click="toggleSelectAll">
        全选<span v-if="totalSelected > 0" class="chip-count">{{ totalSelected }}</span>
      </button>
      <button class="action-chip" :class="{ active: showTagFilter }" @click="toggleTagFilter">标签</button>
      <button v-if="totalSelected > 0" class="action-chip" :class="{ active: showBatchMenu }" @click="showBatchMenu = !showBatchMenu">批量</button>
      <button v-if="totalSelected > 0" class="action-chip cancel" @click="clearSelection">取消</button>
      <button v-if="auth.isAdmin" class="export-btn" @click="exportExcel" title="导出Excel">📥</button>
    </div>
    <div v-if="showTagFilter" class="tag-list-bar">
      <span
        v-for="tag in visibleAllTags"
        :key="tag"
        class="tag-chip"
        :class="{ active: selectedTags.includes(tag) }"
        @click="toggleTag(tag)"
      >{{ tag }}<span v-if="auth.isAdmin" class="tag-delete" @click.stop="deleteGlobalTag(tag)">×</span></span>
      <div v-if="auth.canEdit" class="tag-add-row">
        <input v-model="globalNewTag" class="tag-add-input" placeholder="新标签..." @keydown.enter="addGlobalTag" />
        <button class="tag-add-btn" @click="addGlobalTag" :disabled="!globalNewTag.trim()">+</button>
      </div>
    </div>

    <div v-if="showBatchMenu && totalSelected > 0" class="batch-menu">
      <button class="batch-menu-btn" @click="doBatch('tag')">🏷 批量打标签</button>
      <button class="batch-menu-btn" @click="doBatch('export')">📷 批量导出卡片</button>
      <button v-if="selectedDiaryIds.size > 0 && selectedIds.size === 0" class="batch-menu-btn" @click="doBatch('share_public')">📤 批量共享到公共库</button>
      <button class="batch-menu-btn batch-delete" @click="doBatch('delete')">🗑 批量删除</button>
    </div>
    <!-- Batch Tag Picker -->
    <div v-if="showBatchTagPicker" class="batch-tag-picker">
      <div class="editor-tags">
        <span v-for="tag in batchTagsSelected" :key="tag" class="editor-tag">
          {{ tag }}
          <span class="tag-remove" @click="batchTagsSelected = batchTagsSelected.filter(t => t !== tag)">×</span>
        </span>
      </div>
      <div class="candidate-tags">
        <span
          v-for="tag in visibleAllTags.filter(t => !batchTagsSelected.includes(t))"
          :key="tag"
          class="candidate-tag"
          @click="batchTagsSelected.push(tag)"
        >+ {{ tag }}</span>
      </div>
      <div class="tag-input-row">
        <input v-model="batchNewTag" class="editor-input" placeholder="新标签..." @keydown.enter="addBatchTag" style="flex:1;max-width:120px" />
        <button class="action-btn action-btn-sm" @click="addBatchTag" :disabled="!batchNewTag.trim()">+</button>
      </div>
      <div v-if="batchExistingTags.length" style="margin-top:8px">
        <div style="font-size:12px;color:#999;margin-bottom:4px">点击移除已有标签：</div>
        <div class="editor-tags">
          <span v-for="tag in batchExistingTags" :key="'rm-'+tag" class="editor-tag tag-removable" @click="batchTagsToRemove.includes(tag) ? batchTagsToRemove.splice(batchTagsToRemove.indexOf(tag),1) : batchTagsToRemove.push(tag)" :class="{ 'tag-marked-remove': batchTagsToRemove.includes(tag) }">
            {{ tag }} <span style="margin-left:2px">{{ batchTagsToRemove.includes(tag) ? '✓移除' : '×' }}</span>
          </span>
        </div>
      </div>
      <div style="display:flex;gap:6px;margin-top:8px">
        <button class="action-btn action-btn-primary action-btn-sm" @click="applyBatchTags">确认</button>
        <button class="action-btn action-btn-sm" @click="showBatchTagPicker = false">取消</button>
      </div>
    </div>

    <!-- My Recipes Section (from diary) -->
    <div class="recipe-section">
      <h3 class="section-title clickable" @click="showMyRecipes = !showMyRecipes">
        <button class="mini-select" :class="{ active: isMyAllSelected }" @click.stop="toggleMySelect" title="全选我的配方">✓</button>
        <span>📖 我的配方 ({{ myRecipes.length }})</span>
        <span v-if="!auth.isAdmin" class="contrib-tag">已贡献 {{ sharedCount.adopted }}/{{ sharedCount.total }} 条</span>
        <span class="toggle-icon">{{ showMyRecipes ? '▾' : '▸' }}</span>
      </h3>
      <template v-if="showMyRecipes || manageSearch || selectedTags.length">
      <div class="recipe-list">
        <div
          v-for="d in myFilteredRecipes"
          :key="'diary-' + d.id"
          class="recipe-row"
          :class="{ selected: selectedDiaryIds.has(d.id) }"
        >
          <input
            type="checkbox"
            :checked="selectedDiaryIds.has(d.id)"
            @change="toggleDiarySelect(d.id)"
            class="row-check"
          />
          <div class="row-info" @click="editDiaryRecipe(d)">
            <span class="row-name">{{ d.name }}</span>
            <span v-if="getVolumeLabel(d.ingredients)" class="row-volume">{{ getVolumeLabel(d.ingredients) }}</span>
            <span class="row-tags">
              <span v-for="t in [...(d.tags || [])].sort((a,b)=>a.localeCompare(b,'zh'))" :key="t" class="mini-tag">{{ t }}</span>
            </span>
            <span class="row-cost">{{ oils.fmtPrice(oils.calcCost(d.ingredients || [])) }}</span>
            <span v-if="getDiaryShareStatus(d) === 'shared'" class="share-tag shared">已共享</span>
            <span v-else-if="getDiaryShareStatus(d) === 'pending'" class="share-tag pending">等待审核</span>
          </div>
          <div class="row-actions">
            <button class="btn-icon" @click="handleShare(d)" title="共享到公共配方库">📤</button>
            <button class="btn-icon" @click="removeDiaryRecipe(d)" title="删除">🗑️</button>
          </div>
        </div>
        <div v-if="myFilteredRecipes.length === 0" class="empty-hint">暂无个人配方</div>
      </div>
      </template>
    </div>

    <!-- Public Recipes Section (editor+) -->
    <div v-if="auth.canEdit" class="recipe-section">
      <h3 class="section-title clickable" @click="showPublicRecipes = !showPublicRecipes">
        <button class="mini-select" :class="{ active: isPubAllSelected }" @click.stop="togglePubSelect" title="全选公共配方">✓</button>
        <span>🌿 公共配方库 ({{ publicRecipes.length }})</span>
        <span class="toggle-icon">{{ showPublicRecipes ? '▾' : '▸' }}</span>
      </h3>
      <div v-if="showPublicRecipes || manageSearch || selectedTags.length" class="recipe-list">
        <div
          v-for="r in publicFilteredRecipes"
          :key="r._id"
          class="recipe-row"
          :class="{ selected: selectedIds.has(r._id) }"
        >
          <input
            type="checkbox"
            :checked="selectedIds.has(r._id)"
            @change="toggleSelect(r._id)"
            class="row-check"
          />
          <div class="row-info" @click="editRecipe(r)">
            <span class="row-name">{{ r.name }}</span>
            <span v-if="getVolumeLabel(r.ingredients)" class="row-volume">{{ getVolumeLabel(r.ingredients) }}</span>
            <span class="row-tags">
              <span v-for="t in [...(r.tags || [])].sort((a,b)=>a.localeCompare(b,'zh'))" :key="t" class="mini-tag">{{ t }}</span>
            </span>
            <span class="row-cost">{{ oils.fmtPrice(oils.calcCost(r.ingredients)) }}</span>
          </div>
          <div class="row-actions" v-if="auth.canEditRecipe(r)">
            <button class="btn-icon" @click="removeRecipe(r)" title="删除">🗑️</button>
          </div>
        </div>
        <div v-if="publicFilteredRecipes.length === 0" class="empty-hint">暂无公共配方</div>
      </div>
    </div>

    <!-- Add/Edit Recipe Overlay -->
    <div v-if="showAddOverlay" class="overlay">
      <div class="overlay-panel">
        <div class="overlay-header">
          <h3>{{ editingRecipe ? '编辑配方' : '添加配方' }}</h3>
          <button class="btn-close" @click="closeOverlay">✕</button>
        </div>

        <!-- Smart Paste Section (only for new recipes) -->
        <template v-if="!editingRecipe">
          <div class="paste-section">
            <textarea
              v-model="smartPasteText"
              class="paste-input"
              placeholder="直接粘贴配方文本，支持多条配方同时识别&#10;例如: 舒缓放松，薰衣草3，茶树2&#10;提神醒脑，柠檬5，椒样薄荷3&#10;不写数字默认1滴: 薰衣草，茶树，乳香"
              rows="4"
            ></textarea>
            <button class="btn-primary btn-sm" @click="handleSmartPaste" :disabled="!smartPasteText.trim()">
              🪄 智能识别
            </button>
          </div>

          <!-- Multi-recipe queue indicator -->
          <div v-if="parsedRecipes.length > 0" class="parsed-queue">
            <div class="parsed-queue-header">
              <span class="parsed-queue-label">批量识别 ({{ parsedCurrentIndex + 1 }}/{{ parsedRecipes.length }})</span>
              <button class="btn-outline btn-sm" @click="saveAllParsed">全部保存</button>
              <button class="btn-outline btn-sm" @click="parsedRecipes = []; parsedCurrentIndex = -1">取消全部</button>
            </div>
            <div class="parsed-queue-list">
              <button
                v-for="(pr, pi) in parsedRecipes" :key="pi"
                class="parsed-queue-item"
                :class="{ active: pi === parsedCurrentIndex }"
                @click="loadParsedIntoForm(pi)"
              >
                <span class="parsed-queue-name">{{ pr.name || '未命名' }}</span>
                <span class="parsed-queue-count">{{ pr.ingredients.length }}味</span>
                <span class="btn-icon-sm" @click.stop="parsedRecipes.splice(pi, 1); if (parsedRecipes.length === 0) parsedCurrentIndex = -1; else if (pi <= parsedCurrentIndex) loadParsedIntoForm(Math.min(parsedCurrentIndex, parsedRecipes.length - 1))">✕</span>
              </button>
            </div>
          </div>

          <div class="divider-text">或手动输入</div>
        </template>

        <!-- Manual Form (matches RecipeDetailOverlay editor) -->
        <div class="editor-header">
          <div style="flex:1;min-width:0">
            <input v-model="formName" type="text" class="editor-name-input" placeholder="配方名称" />
          </div>
          <div class="editor-header-actions">
            <button class="action-btn action-btn-primary action-btn-sm" @click="saveCurrentRecipe">💾 保存</button>
            <button class="action-btn action-btn-sm" @click="previewRecipe">👁 预览</button>
          </div>
        </div>

        <!-- Volume selector -->
        <div class="editor-section">
          <label class="editor-label">容量</label>
          <div class="volume-controls">
            <button class="volume-btn" :class="{ active: formVolume === 'single' }" @click="formVolume = 'single'">单次</button>
            <button class="volume-btn" :class="{ active: formVolume === '5' }" @click="formVolume = '5'">5ml</button>
            <button class="volume-btn" :class="{ active: formVolume === '10' }" @click="formVolume = '10'">10ml</button>
            <button class="volume-btn" :class="{ active: formVolume === '15' }" @click="formVolume = '15'">15ml</button>
            <button class="volume-btn" :class="{ active: formVolume === '20' }" @click="formVolume = '20'">20ml</button>
            <button class="volume-btn" :class="{ active: formVolume === '30' }" @click="formVolume = '30'">30ml</button>
            <button class="volume-btn" :class="{ active: formVolume === 'custom' }" @click="formVolume = 'custom'">自定义</button>
          </div>
          <div v-if="formVolume === 'custom'" class="custom-volume-row">
            <input v-model.number="formCustomVolume" type="number" min="1" class="drops-sm" placeholder="ml" />
            <span style="font-size:12px;color:#999">ml</span>
          </div>
          <div class="ratio-row">
            <span class="ratio-label">参考比例 1:</span>
            <select v-model.number="formDilution" class="select-sm">
              <option v-for="n in [3,4,5,6,7,8,9,10,12,15,20]" :key="n" :value="n">{{ n }}</option>
            </select>
            <span class="ratio-hint">时，纯精油总数约为 {{ suggestedEoDrops }} 滴，现在为 {{ eoTotalDrops }} 滴</span>
          </div>
        </div>

        <!-- Ingredients table (essential oils only, coconut at bottom) -->
        <div class="editor-section">
          <table class="editor-table">
            <thead>
              <tr><th>精油</th><th>滴数</th><th>单价/滴</th><th>小计</th><th></th></tr>
            </thead>
            <tbody>
              <tr v-for="(ing, i) in formEoIngredients" :key="'eo-'+i">
                <td>
                  <div class="oil-search-wrap">
                    <input
                      v-model="ing._search"
                      class="form-select"
                      placeholder="搜索精油..."
                      @focus="ing._open = true"
                      @input="ing._open = true"
                      @blur="onOilBlur(ing)"
                    />
                    <div v-if="ing._open" class="oil-dropdown">
                      <div
                        v-for="name in filteredOilNames(ing._search || '')"
                        :key="name"
                        class="oil-option"
                        @mousedown.prevent="selectOil(ing, name)"
                      >{{ name }}</div>
                      <div v-if="filteredOilNames(ing._search || '').length === 0" class="oil-option oil-empty">无匹配</div>
                    </div>
                  </div>
                </td>
                <td><input v-model.number="ing.drops" type="number" min="0.5" step="0.5" class="editor-drops" /></td>
                <td class="ing-ppd">{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil)) : '-' }}</td>
                <td class="ing-cost">{{ ing.oil && ing.drops ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * ing.drops) : '-' }}</td>
                <td><button class="remove-row-btn" @click="removeEoRow(i)">✕</button></td>
              </tr>
              <!-- Coconut oil row -->
              <tr v-if="formCocoRow" class="coco-row">
                <td><span class="coco-label">椰子油</span></td>
                <td>
                  <template v-if="formVolume === 'single'">
                    <input v-model.number="formCocoRow.drops" type="number" min="0" class="editor-drops" />
                  </template>
                  <template v-else>
                    <span class="coco-fill">填满 (约{{ cocoFillMl }}ml)</span>
                  </template>
                </td>
                <td class="ing-ppd">{{ oils.fmtPrice(oils.pricePerDrop('椰子油')) }}</td>
                <td class="ing-cost">{{ oils.fmtPrice(oils.pricePerDrop('椰子油') * cocoActualDrops) }}</td>
                <td><button class="remove-row-btn" @click="formCocoRow = null">✕</button></td>
              </tr>
            </tbody>
          </table>
          <button class="add-row-btn" @click="addOilRow">+ 添加精油</button>
        </div>

        <!-- Real-time summary (only when volume selected) -->
        <div v-if="formVolume" class="recipe-summary">
          {{ recipeSummaryText }}
        </div>

        <!-- Notes -->
        <div class="editor-section">
          <label class="editor-label">备注</label>
          <textarea v-model="formNote" class="editor-textarea" rows="2" placeholder="配方备注..."></textarea>
        </div>

        <!-- Tags -->
        <div class="editor-section">
          <label class="editor-label">标签</label>
          <div class="editor-tags">
            <span v-for="tag in formTags" :key="tag" class="editor-tag">
              {{ tag }}
              <span class="tag-remove" @click="toggleFormTag(tag)">×</span>
            </span>
          </div>
          <div class="candidate-tags" v-if="formCandidateTags.length">
            <span v-for="tag in formCandidateTags" :key="tag" class="candidate-tag" @click="toggleFormTag(tag)">+ {{ tag }}</span>
          </div>
          <div class="tag-input-row">
            <input v-model="newTagInput" type="text" class="editor-input" placeholder="添加新标签..." @keydown.enter="addNewFormTag" style="flex:1" />
            <button class="action-btn action-btn-sm" @click="addNewFormTag" :disabled="!newTagInput.trim()">+</button>
          </div>
        </div>

        <!-- Total cost -->
        <div class="editor-total">
          总计: {{ formTotalCost }}
        </div>
      </div>
    </div>

    <!-- Review History (admin only) -->
    <div v-if="auth.isAdmin" class="recipe-section">
      <h3 class="section-title clickable" @click="showReviewHistory = !showReviewHistory">
        <span>📋 审核记录</span>
        <span class="toggle-icon">{{ showReviewHistory ? '▾' : '▸' }}</span>
      </h3>
      <div v-if="showReviewHistory" class="review-history">
        <div v-for="r in reviewHistory" :key="r.id" class="review-log-item">
          <span :class="r.action === 'adopt_recipe' ? 'log-approve' : 'log-reject'">
            {{ r.action === 'adopt_recipe' ? '✅ 采纳' : '❌ 拒绝' }}
          </span>
          <span class="log-recipe">{{ r.target_name }}</span>
          <span class="log-from" v-if="r.detail">{{ parseReviewDetail(r.detail) }}</span>
          <span class="log-time">{{ formatDate(r.created_at) }}</span>
        </div>
        <div v-if="reviewHistory.length === 0" class="empty-hint">暂无审核记录</div>
      </div>
    </div>

    <!-- Recipe Detail Overlay -->
    <RecipeDetailOverlay
      v-if="previewRecipeIndex !== null || previewRecipeData !== null"
      :recipeIndex="previewRecipeIndex"
      :recipeData="previewRecipeData"
      :isDiary="true"
      @close="previewRecipeIndex = null; previewRecipeData = null"
    />

    <!-- Tag Picker Overlay -->
    <TagPicker
      v-if="showTagPicker"
      :name="tagPickerName"
      :currentTags="tagPickerTags"
      :allTags="recipeStore.allTags"
      @save="onTagPickerSave"
      @close="showTagPicker = false"
    />
    </template>
  </div>
</template>

<script setup>
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useOilsStore } from '../stores/oils'
import { useRecipesStore, EDITOR_ONLY_TAGS } from '../stores/recipes'
import { useDiaryStore } from '../stores/diary'
import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
import { showConfirm, showPrompt } from '../composables/useDialog'
import { parseSingleBlock, parseMultiRecipes } from '../composables/useSmartPaste'
import { matchesPinyinInitials } from '../composables/usePinyinMatch'
import RecipeCard from '../components/RecipeCard.vue'
import TagPicker from '../components/TagPicker.vue'
import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue'

const auth = useAuthStore()
const oils = useOilsStore()
const recipeStore = useRecipesStore()
const diaryStore = useDiaryStore()
const ui = useUiStore()

const manageSearch = ref('')
const selectedTags = ref([])
const showTagFilter = ref(false)
const selectedIds = reactive(new Set())
const selectedDiaryIds = reactive(new Set())
const showAddOverlay = ref(false)
const editingRecipe = ref(null)
const showPending = ref(false)
const pendingRecipes = ref([])
const pendingCount = ref(0)
const seniorEditors = ref([])

// Form state
const formName = ref('')
const formIngredients = ref([{ oil: '', drops: 1, _search: '', _open: false }])
const formNote = ref('')
const formTags = ref([])
const smartPasteText = ref('')
const parsedRecipes = ref([])
const parsedCurrentIndex = ref(-1)
const showAddIngRow = ref(false)
const newIngOil = ref('')
const newIngSearch = ref('')
const newIngDrops = ref(1)
const newIngDropdownOpen = ref(false)
const formVolume = ref('')
const formCustomVolume = ref(null)
const formCustomUnit = ref('drops')
const formDilution = ref(6)

const formCocoRow = ref(null)

watch(() => formVolume.value, (vol) => {
  if (vol && !formCocoRow.value && parsedCurrentIndex.value < 0) {
    formCocoRow.value = { oil: '椰子油', drops: vol === 'single' ? 10 : 0, _search: '椰子油', _open: false }
  }
})

// EO ingredients (everything except coconut)
const formEoIngredients = computed(() =>
  formIngredients.value.filter(i => i.oil !== '椰子油')
)

const eoTotalDrops = computed(() =>
  formEoIngredients.value.filter(i => i.oil && i.drops > 0).reduce((s, i) => s + i.drops, 0)
)

const targetTotalDrops = computed(() => {
  if (formVolume.value === 'single') return null
  if (formVolume.value === 'custom') return Math.round((formCustomVolume.value || 0) * DROPS_PER_ML)
  return Math.round(Number(formVolume.value) * DROPS_PER_ML)
})

const cocoActualDrops = computed(() => {
  if (!formCocoRow.value) return 0
  if (formVolume.value === 'single') return formCocoRow.value.drops || 0
  if (!targetTotalDrops.value) return 0
  return Math.max(0, targetTotalDrops.value - eoTotalDrops.value)
})

const cocoFillMl = computed(() => Math.round(cocoActualDrops.value / DROPS_PER_ML))

const suggestedEoDrops = computed(() => {
  if (formVolume.value === 'single') {
    const cocoDrops = formCocoRow.value ? (formCocoRow.value.drops || 10) : 10
    return Math.round(cocoDrops / formDilution.value)
  }
  const total = targetTotalDrops.value || 0
  return Math.round(total / (1 + formDilution.value))
})

const recipeSummaryText = computed(() => {
  const eo = eoTotalDrops.value
  const coco = cocoActualDrops.value
  const ratio = eo > 0 ? Math.round(coco / eo) : 0
  if (formVolume.value === 'single') {
    return `该配方为单次用量，纯精油 ${eo} 滴，椰子油 ${coco} 滴，稀释比例 1:${ratio}`
  }
  const vol = formVolume.value === 'custom' ? (formCustomVolume.value || 0) : Number(formVolume.value)
  return `该配方总容量 ${vol}ml，纯精油 ${eo} 滴，剩余用椰子油填满，稀释比例 1:${ratio}`
})

const formTotalCost = computed(() => {
  let cost = formIngredients.value
    .filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
    .reduce((sum, i) => sum + oils.pricePerDrop(i.oil) * i.drops, 0)
  cost += oils.pricePerDrop('椰子油') * cocoActualDrops.value
  return oils.fmtPrice(cost)
})

// Tag picker state
const showTagPicker = ref(false)
const tagPickerName = ref('')
const tagPickerTags = ref([])

// Computed lists
// "我的配方" = diary (user_diary table), personal recipes
const myRecipes = computed(() => diaryStore.userDiary)

// "公共配方库" = all recipes in public library (recipes table)
const publicRecipes = computed(() => recipeStore.recipes)

function filterBySearchAndTags(list) {
  let result = list
  const q = manageSearch.value.trim().toLowerCase()
  if (q) {
    result = result.filter(r =>
      r.name.toLowerCase().includes(q) ||
      (r.ingredients || []).some(ing => (ing.oil || '').toLowerCase().includes(q)) ||
      (r.tags && r.tags.some(t => t.toLowerCase().includes(q)))
    )
  }
  if (selectedTags.value.length > 0) {
    result = result.filter(r =>
      r.tags && selectedTags.value.every(t => r.tags.includes(t))
    )
  }
  return result.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'))
}

const myFilteredRecipes = computed(() => filterBySearchAndTags(myRecipes.value))
const publicFilteredRecipes = computed(() => filterBySearchAndTags(publicRecipes.value))

const globalNewTag = ref('')

async function addGlobalTag() {
  const tag = globalNewTag.value.trim()
  if (!tag) return
  try {
    await api('/api/tags', { method: 'POST', body: JSON.stringify({ name: tag }) })
    if (!recipeStore.allTags.includes(tag)) {
      recipeStore.allTags.push(tag)
      recipeStore.allTags.sort((a, b) => a.localeCompare(b, 'zh'))
    }
    globalNewTag.value = ''
    ui.showToast('标签已添加')
  } catch {
    ui.showToast('添加失败')
  }
}

async function deleteGlobalTag(tag) {
  const { showConfirm } = await import('../composables/useDialog')
  const ok = await showConfirm(`确定删除标签「${tag}」？`)
  if (!ok) return
  try {
    await api(`/api/tags/${encodeURIComponent(tag)}`, { method: 'DELETE' })
    const idx = recipeStore.allTags.indexOf(tag)
    if (idx >= 0) recipeStore.allTags.splice(idx, 1)
    ui.showToast('标签已删除')
  } catch {
    ui.showToast('删除失败')
  }
}

function toggleTagFilter() {
  if (showTagFilter.value) {
    showTagFilter.value = false
    selectedTags.value = []
  } else {
    showTagFilter.value = true
  }
}

function toggleTag(tag) {
  const idx = selectedTags.value.indexOf(tag)
  if (idx >= 0) selectedTags.value.splice(idx, 1)
  else selectedTags.value.push(tag)
}

function toggleSelect(id) {
  if (selectedIds.has(id)) selectedIds.delete(id)
  else selectedIds.add(id)
}

function toggleDiarySelect(id) {
  if (selectedDiaryIds.has(id)) selectedDiaryIds.delete(id)
  else selectedDiaryIds.add(id)
}

function clearSelection() {
  selectedIds.clear()
  selectedDiaryIds.clear()
  showBatchMenu.value = false
}

function addBatchTag() {
  const tag = batchNewTag.value.trim()
  if (tag && !batchTagsSelected.value.includes(tag)) {
    batchTagsSelected.value.push(tag)
  }
  batchNewTag.value = ''
}

async function applyBatchTags() {
  const tagsToAdd = batchTagsSelected.value
  const tagsToRemove = batchTagsToRemove.value
  if (!tagsToAdd.length && !tagsToRemove.length) { ui.showToast('请选择要添加或移除的标签'); return }
  const pubIds = [...selectedIds]
  const diaryIds = [...selectedDiaryIds]
  for (const id of pubIds) {
    const recipe = recipeStore.recipes.find(r => r._id === id)
    if (!recipe) continue
    let newTags = [...recipe.tags]
    let changed = false
    for (const t of tagsToAdd) {
      if (!newTags.includes(t)) { newTags.push(t); changed = true }
    }
    for (const t of tagsToRemove) {
      const idx = newTags.indexOf(t)
      if (idx >= 0) { newTags.splice(idx, 1); changed = true }
    }
    if (changed) {
      await api(`/api/recipes/${recipe._id}`, {
        method: 'PUT',
        body: JSON.stringify({ tags: newTags }),
      })
      recipe.tags = newTags
    }
  }
  for (const id of diaryIds) {
    const d = diaryStore.userDiary.find(r => r.id === id)
    if (!d) continue
    let dtags = [...(d.tags || [])]
    let changed = false
    for (const t of tagsToAdd) {
      if (!dtags.includes(t)) { dtags.push(t); changed = true }
    }
    for (const t of tagsToRemove) {
      const idx = dtags.indexOf(t)
      if (idx >= 0) { dtags.splice(idx, 1); changed = true }
    }
    if (changed) await diaryStore.updateDiary(id, { ...d, tags: dtags })
  }
  showBatchTagPicker.value = false
  const msgs = []
  if (tagsToAdd.length) msgs.push(`添加${tagsToAdd.length}个`)
  if (tagsToRemove.length) msgs.push(`移除${tagsToRemove.length}个`)
  ui.showToast(`已为 ${pubIds.length + diaryIds.length} 个配方${msgs.join('、')}标签`)
  clearSelection()
}

function previewRecipe() {
  const eoIngs = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
  const cleanIngs = eoIngs.map(i => ({ oil: i.oil, drops: i.drops }))
  if (formCocoRow.value && cocoActualDrops.value > 0) {
    cleanIngs.push({ oil: '椰子油', drops: cocoActualDrops.value })
  }
  previewRecipeData.value = {
    _id: null,
    name: formName.value || '未命名配方',
    note: formNote.value || '',
    tags: formTags.value,
    ingredients: cleanIngs,
  }
}

function doBatch(action) {
  showBatchMenu.value = false
  executeBatchAction(action)
}

function toggleSelectAll() {
  if (isAllSelected.value) {
    clearSelection()
  } else {
    myFilteredRecipes.value.forEach(d => selectedDiaryIds.add(d.id))
    if (auth.canEdit) publicFilteredRecipes.value.forEach(r => selectedIds.add(r._id))
    showMyRecipes.value = true
    if (auth.canEdit) showPublicRecipes.value = true
  }
}

function toggleMySelect() {
  if (isMyAllSelected.value) {
    selectedDiaryIds.clear()
  } else {
    myFilteredRecipes.value.forEach(d => selectedDiaryIds.add(d.id))
    showMyRecipes.value = true
  }
}

function togglePubSelect() {
  if (isPubAllSelected.value) {
    selectedIds.clear()
  } else {
    publicFilteredRecipes.value.forEach(r => selectedIds.add(r._id))
    showPublicRecipes.value = true
  }
}

async function executeBatchAction(action) {
  const pubIds = [...selectedIds]
  const diaryIds = [...selectedDiaryIds]
  const totalCount = pubIds.length + diaryIds.length
  if (!totalCount) return

  if (action === 'delete') {
    const ok = await showConfirm(`确定删除 ${totalCount} 个配方？`)
    if (!ok) return
    for (const id of pubIds) {
      await recipeStore.deleteRecipe(id)
    }
    for (const id of diaryIds) {
      await diaryStore.deleteDiary(id)
    }
    ui.showToast(`已删除 ${totalCount} 个配方`)
  } else if (action === 'tag') {
    batchTagsSelected.value = []
    batchTagsToRemove.value = []
    batchNewTag.value = ''
    showBatchTagPicker.value = true
    return // don't clear selection yet
  } else if (action === 'share_public') {
    const ok = await showConfirm(`将 ${diaryIds.length} 个配方分享到公共配方库？`)
    if (!ok) return
    let count = 0
    for (const id of diaryIds) {
      const d = diaryStore.userDiary.find(r => r.id === id)
      if (!d) continue
      try {
        await api('/api/recipes', {
          method: 'POST',
          body: JSON.stringify({
            name: d.name,
            note: d.note || '',
            ingredients: (d.ingredients || []).map(i => ({ oil_name: i.oil, drops: i.drops })),
            tags: d.tags || [],
          }),
        })
        count++
      } catch {}
    }
    await recipeStore.loadRecipes()
    ui.showToast(`已提交 ${count} 个配方，等待审核`)
  } else if (action === 'export') {
    ui.showToast('导出卡片功能开发中')
  }
  clearSelection()
}

function calcDilutionFromIngs() {
  // No default selection — user chooses
}

function editRecipe(recipe) {
  editingRecipe.value = { _id: recipe._id, _version: recipe._version, name: recipe.name }
  formName.value = recipe.name
  const ings = recipe.ingredients || []
  formIngredients.value = ings.filter(i => i.oil !== '椰子油').map(i => ({ ...i, _search: i.oil, _open: false }))
  const coco = ings.find(i => i.oil === '椰子油')
  if (coco) {
    formCocoRow.value = { ...coco, _search: '椰子油', _open: false }
    // Guess volume from total drops
    const eoDrops = ings.filter(i => i.oil && i.oil !== '椰子油').reduce((s, i) => s + (i.drops || 0), 0)
    const totalDrops = eoDrops + coco.drops
    const ml = totalDrops / DROPS_PER_ML
    if (ml <= 2) formVolume.value = 'single'
    else if (Math.abs(ml - 5) < 1.5) formVolume.value = '5'
    else if (Math.abs(ml - 10) < 2.5) formVolume.value = '10'
    else if (Math.abs(ml - 15) < 2.5) formVolume.value = '15'
    else if (Math.abs(ml - 20) < 3) formVolume.value = '20'
    else if (Math.abs(ml - 30) < 6) formVolume.value = '30'
    else { formVolume.value = 'custom'; formCustomVolume.value = Math.round(ml) }
    // Guess dilution
    if (eoDrops > 0 && coco.drops > 0) {
      const ratio = Math.round(coco.drops / eoDrops)
      const options = [3,4,5,6,7,8,9,10,12,15,20]
      formDilution.value = options.reduce((a, b) => Math.abs(b - ratio) < Math.abs(a - ratio) ? b : a)
    }
  } else {
    formCocoRow.value = null
    formVolume.value = ''
  }
  formNote.value = recipe.note || ''
  formTags.value = [...(recipe.tags || [])]
  showAddOverlay.value = true
}

function closeOverlay() {
  showAddOverlay.value = false
  editingRecipe.value = null
  resetForm()
}

function resetForm() {
  formName.value = ''
  formIngredients.value = [{ oil: '', drops: 1, _search: '', _open: false }]
  formCocoRow.value = null
  formNote.value = ''
  formTags.value = []
  smartPasteText.value = ''
  parsedRecipes.value = []
  parsedCurrentIndex.value = -1
  showAddIngRow.value = false
  newIngOil.value = ''
  newIngSearch.value = ''
  newIngDrops.value = 1
  formVolume.value = ''
  formCustomVolume.value = null
  formDilution.value = 6
}

function handleSmartPaste() {
  const results = parseMultiRecipes(smartPasteText.value, oils.oilNames)
  if (results.length === 0) {
    ui.showToast('未能识别出任何配方')
    return
  }
  if (results.length === 1) {
    // Single recipe: populate form directly
    const r = results[0]
    formName.value = r.name
    formIngredients.value = r.ingredients.length > 0
      ? r.ingredients.map(i => ({ ...i, _search: i.oil, _open: false }))
      : [{ oil: '', drops: 1, _search: '', _open: false }]
    if (r.notFound.length > 0) {
      ui.showToast(`未识别: ${r.notFound.join('、')}`)
    }
    parsedRecipes.value = []
  } else {
    // Multiple recipes: store queue, load first into form
    parsedRecipes.value = results
    loadParsedIntoForm(0)
    ui.showToast(`识别出 ${results.length} 条配方，逐条编辑保存`)
  }
}

function filteredOilNames(search) {
  if (!search) return oils.oilNames
  const q = search.toLowerCase()
  const results = oils.oilNames.filter(name =>
    name.toLowerCase().includes(q) || matchesPinyinInitials(name, q)
  )
  // Sort: pinyin prefix match first, then name contains, then rest
  results.sort((a, b) => {
    const aPin = matchesPinyinInitials(a, q) ? 0 : 1
    const bPin = matchesPinyinInitials(b, q) ? 0 : 1
    if (aPin !== bPin) return aPin - bPin
    return a.localeCompare(b, 'zh')
  })
  return results
}

function selectOil(ing, name) {
  // Check for duplicate oil in current recipe
  const existing = formIngredients.value.find(i => i !== ing && i.oil === name)
  if (existing) {
    ui.showToast(`已有「${name}」，请直接修改滴数`)
    ing._open = false
    return
  }
  ing.oil = name
  ing._search = name
  ing._open = false
}

function onOilBlur(ing) {
  setTimeout(() => {
    ing._open = false
    if (!ing.oil) ing._search = ''
    else ing._search = ing.oil
  }, 150)
}

const newTagInput = ref('')

const formCandidateTags = computed(() =>
  recipeStore.allTags.filter(t => !formTags.value.includes(t))
)

function addNewFormTag() {
  const tag = newTagInput.value.trim()
  if (!tag) return
  if (!formTags.value.includes(tag)) {
    formTags.value.push(tag)
  }
  // Also add to global tags so it appears in candidates if removed
  if (!recipeStore.allTags.includes(tag)) {
    recipeStore.allTags.push(tag)
    recipeStore.allTags.sort((a, b) => a.localeCompare(b, 'zh'))
  }
  newTagInput.value = ''
}

const DROPS_PER_ML = 18.6

const formDilutionHint = computed(() => {
  const eoIngs = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
  const eoDrops = eoIngs.reduce((s, i) => s + i.drops, 0)
  if (formVolume.value === 'single') {
    const cocoDrops = Math.round(eoDrops * formDilution.value)
    const totalDrops = eoDrops + cocoDrops
    return `单次用量：纯精油约 ${eoDrops} 滴 + 椰子油约 ${cocoDrops} 滴，共 ${totalDrops} 滴 (${Math.round(totalDrops / DROPS_PER_ML)}ml)，稀释 1:${formDilution.value}`
  }
  let totalDrops
  if (formVolume.value === 'custom') {
    totalDrops = formCustomUnit.value === 'ml' ? Math.round(formCustomVolume.value * DROPS_PER_ML) : formCustomVolume.value
  } else {
    totalDrops = Math.round(Number(formVolume.value) * DROPS_PER_ML)
  }
  const targetEo = Math.round(totalDrops / (1 + formDilution.value))
  const cocoDrops = totalDrops - targetEo
  return `总容量 ${totalDrops} 滴 (${Math.round(totalDrops / DROPS_PER_ML)}ml)，纯精油约 ${targetEo} 滴 + 椰子油约 ${cocoDrops} 滴，稀释 1:${formDilution.value}`
})

function applyVolumeDilution() {
  const eoIngs = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油')
  if (eoIngs.length === 0) { ui.showToast('请先添加精油'); return }

  let targetTotalDrops
  if (formVolume.value === 'single') {
    const targetEoDrops = 10
    const currentEoTotal = eoIngs.reduce((s, i) => s + (i.drops || 0), 0)
    if (currentEoTotal <= 0) return
    const scale = targetEoDrops / currentEoTotal
    eoIngs.forEach(i => { i.drops = Math.max(0.5, Math.round(i.drops * scale * 2) / 2) })
    const actualEo = eoIngs.reduce((s, i) => s + i.drops, 0)
    setFormCoconut(actualEo * formDilution.value)
    ui.showToast('已应用单次用量')
    return
  }

  if (formVolume.value === 'custom') {
    targetTotalDrops = formCustomUnit.value === 'ml' ? Math.round(formCustomVolume.value * DROPS_PER_ML) : formCustomVolume.value
  } else {
    targetTotalDrops = Math.round(Number(formVolume.value) * DROPS_PER_ML)
  }

  const targetEoDrops = Math.round(targetTotalDrops / (1 + formDilution.value))
  const currentEoTotal = eoIngs.reduce((s, i) => s + (i.drops || 0), 0)
  if (currentEoTotal <= 0) return
  const scale = targetEoDrops / currentEoTotal
  eoIngs.forEach(i => { i.drops = Math.max(1, Math.round(i.drops * scale)) })
  const actualEo = eoIngs.reduce((s, i) => s + i.drops, 0)
  setFormCoconut(targetTotalDrops - actualEo)
  ui.showToast('已应用容量设置')
}

function setFormCoconut(drops) {
  drops = Math.max(0, Math.round(drops))
  const idx = formIngredients.value.findIndex(i => i.oil === '椰子油')
  if (idx >= 0) {
    formIngredients.value[idx].drops = drops
  } else if (drops > 0) {
    formIngredients.value.push({ oil: '椰子油', drops, _search: '椰子油', _open: false })
  }
}

function addOilRow() {
  formIngredients.value.push({ oil: '', drops: 1, _search: '', _open: false })
}

function removeEoRow(index) {
  // Find the actual index in formIngredients (skip coconut)
  const eoIngs = formIngredients.value.filter(i => i.oil !== '椰子油')
  const target = eoIngs[index]
  const realIdx = formIngredients.value.indexOf(target)
  if (realIdx >= 0) formIngredients.value.splice(realIdx, 1)
}

function confirmAddIng() {
  if (!newIngOil.value || !newIngDrops.value) return
  formIngredients.value.push({ oil: newIngOil.value, drops: newIngDrops.value, _search: newIngOil.value, _open: false })
  newIngOil.value = ''
  newIngSearch.value = ''
  newIngDrops.value = 1
  showAddIngRow.value = false
}

function toggleFormTag(tag) {
  const idx = formTags.value.indexOf(tag)
  if (idx >= 0) formTags.value.splice(idx, 1)
  else formTags.value.push(tag)
}

/**
 * Check name against public + personal recipes.
 * Same name + same content → toast and return false.
 * Same name + different content → show diff, prompt rename, loop until unique.
 * Returns final name or false if cancelled.
 */
async function checkDupName(name, ings, target = 'diary') {
  let currentName = name
  while (true) {
    const dup = target === 'public'
      ? recipeStore.recipes.find(r => r.name === currentName)
      : diaryStore.userDiary.find(d => d.name === currentName)
    if (!dup) return currentName

    const dupIngs = (dup.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
    const myIngs = ings.filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
    const identical = dupIngs.length === myIngs.length && dupIngs.every((d, i) => d.oil === myIngs[i].oil && d.drops === myIngs[i].drops)
    const where = target === 'public' ? '公共配方库' : '我的配方'

    if (identical) {
      ui.showToast(`${where}中已有一模一样的配方「${currentName}」，已跳过`)
      return false
    }

    const existIngs = dupIngs.map(i => `${i.oil}${i.drops}${oils.unitLabel(i.oil)}`).join('、')
    const newIngs = myIngs.map(i => `${i.oil}${i.drops}${oils.unitLabel(i.oil)}`).join('、')
    const ok = await showConfirm(
      `${where}中已有同名配方「${currentName}」，内容不同：\n\n已有：${existIngs}\n新的：${newIngs}\n\n请改名后保存`,
      { okText: '改名', cancelText: '取消' }
    )
    if (!ok) return false
    const newName = await showPrompt('请输入新名称：', currentName)
    if (!newName || !newName.trim()) return false
    currentName = newName.trim()
    // Loop back to check the new name
  }
}

async function saveCurrentRecipe() {
  if (formVolume.value === 'custom' && !formCustomVolume.value) {
    ui.showToast('请输入自定义容量')
    return
  }
  const eoIngs = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0)
  if (!formName.value.trim()) {
    ui.showToast('请输入配方名称')
    return
  }
  if (eoIngs.length === 0) {
    ui.showToast('请至少添加一个精油')
    return
  }

  // Combine EO + coconut
  const cleanIngs = eoIngs.map(i => ({ oil: i.oil, drops: i.drops }))
  if (formCocoRow.value && cocoActualDrops.value > 0) {
    cleanIngs.push({ oil: '椰子油', drops: cocoActualDrops.value })
  }
  const diaryPayload = {
    name: formName.value.trim(),
    ingredients: cleanIngs,
    note: formNote.value,
    tags: formTags.value,
  }

  if (editingRecipe.value && editingRecipe.value._diary_id) {
    // Editing an existing diary recipe
    try {
      await diaryStore.updateDiary(editingRecipe.value._diary_id, diaryPayload)
      ui.showToast('个人配方已更新')
      closeOverlay()
    } catch (e) {
      ui.showToast('保存失败: ' + (e.message || '未知错误'))
    }
    return
  }

  if (editingRecipe.value && editingRecipe.value._id) {
    // Editing an existing public recipe — safety check
    const mappedIngs = cleanIngs.map(i => ({ oil_name: i.oil, drops: i.drops }))
    if (mappedIngs.length === 0) {
      const ok = await showConfirm('配方中没有精油成分，确定保存吗？这将清空所有成分。')
      if (!ok) return
    }
    const payload = {
      _id: editingRecipe.value._id,
      _version: editingRecipe.value._version,
      name: formName.value.trim(),
      ingredients: mappedIngs,
      note: formNote.value,
      tags: formTags.value,
    }
    try {
      await recipeStore.saveRecipe(payload)
      ui.showToast('配方已更新')
      closeOverlay()
    } catch (e) {
      ui.showToast('保存失败: ' + (e.message || '未知错误'))
    }
    return
  }

  // New recipe: admin/senior_editor can choose public or personal
  if (auth.canManage) {
    const toPublic = await showConfirm('保存到哪里？', { okText: '公共配方库', cancelText: '个人配方' })
    if (toPublic) {
      const finalName = await dedupOrSkip(diaryPayload.name, cleanIngs, 'public')
      if (!finalName) return
      try {
        await recipeStore.saveRecipe({
          name: finalName,
          ingredients: cleanIngs.map(i => ({ oil_name: i.oil, drops: i.drops })),
          note: formNote.value,
          tags: formTags.value,
        })
        ui.showToast('已添加到公共配方库')
        if (!loadNextParsed()) closeOverlay()
      } catch (e) {
        ui.showToast('保存失败: ' + (e.message || '未知错误'))
      }
      return
    }
  }
  const finalName = await dedupOrSkip(diaryPayload.name, cleanIngs, 'diary')
  if (!finalName) return
  diaryPayload.name = finalName
  formName.value = finalName
  try {
    await diaryStore.createDiary(diaryPayload)
    ui.showToast('已添加到我的配方')
    if (!loadNextParsed()) closeOverlay()
  } catch (e) {
    ui.showToast('保存失败: ' + (e.message || '未知错误'))
  }
}

async function saveParsedRecipe(index) {
  const r = parsedRecipes.value[index]
  if (!r.name.trim() || r.ingredients.length === 0) {
    ui.showToast('配方名称和成分不能为空')
    return
  }
  try {
    await diaryStore.createDiary({
      name: r.name.trim(),
      ingredients: r.ingredients.map(i => ({ oil: i.oil, drops: i.drops })),
      note: '',
      tags: [],
    })
    parsedRecipes.value.splice(index, 1)
    ui.showToast(`「${r.name}」已保存到我的配方`)
  } catch (e) {
    ui.showToast('保存失败: ' + (e?.message || '未知错误'))
  }
}

async function saveAllParsed() {
  // Sync current form edits back first
  syncFormToParsed()
  const toPublic = auth.canManage && await showConfirm('全部保存到哪里？', { okText: '公共配方库', cancelText: '个人配方' })
  let saved = 0, skipped = 0
  for (let i = 0; i < parsedRecipes.value.length; i++) {
    const r = parsedRecipes.value[i]
    if (!r.name.trim() || r.ingredients.length === 0) continue
    const ings = r.ingredients.map(ing => ({ oil: ing.oil, drops: ing.drops }))
    const finalName = await checkDupName(r.name.trim(), ings, toPublic ? 'public' : 'diary')
    if (finalName === false) { skipped++; continue }
    try {
      if (toPublic) {
        await recipeStore.saveRecipe({
          name: finalName,
          ingredients: ings.map(ing => ({ oil_name: ing.oil, drops: ing.drops })),
          note: '',
          tags: [],
        })
      } else {
        await diaryStore.createDiary({ name: finalName, ingredients: ings, note: '', tags: [] })
      }
      saved++
    } catch {}
  }
  parsedRecipes.value = []
  parsedCurrentIndex.value = -1
  ui.showToast(`已保存 ${saved} 条配方到${toPublic ? '公共配方库' : '我的配方'}`)
  closeOverlay()
}

/** Skip current parsed recipe and load next, or close if none left. */
function skipCurrentParsed() {
  if (parsedCurrentIndex.value < 0) return
  const skipIdx = parsedCurrentIndex.value
  parsedCurrentIndex.value = -1
  parsedRecipes.value.splice(skipIdx, 1)
  if (parsedRecipes.value.length > 0) {
    const next = Math.min(skipIdx, parsedRecipes.value.length - 1)
    parsedCurrentIndex.value = next
    const r = parsedRecipes.value[next]
    formName.value = r.name
    const cocoIng = r.ingredients.find(i => i.oil === '椰子油')
    const eoIngs = r.ingredients.filter(i => i.oil !== '椰子油')
    formIngredients.value = eoIngs.length > 0
      ? eoIngs.map(i => ({ ...i, _search: i.oil, _open: false }))
      : [{ oil: '', drops: 1, _search: '', _open: false }]
    if (cocoIng) {
      formCocoRow.value = { oil: '椰子油', drops: cocoIng.drops, _search: '椰子油', _open: false }
      formVolume.value = 'single'
    } else {
      formCocoRow.value = null
      formVolume.value = ''
    }
  } else {
    closeOverlay()
  }
}

/**
 * Run dedup check for saveCurrentRecipe. Returns final name or null if should stop.
 */
async function dedupOrSkip(name, ings, target) {
  if (editingRecipe.value) return name
  const result = await checkDupName(name, ings, target)
  if (result === false) {
    if (parsedCurrentIndex.value >= 0) skipCurrentParsed()
    return null
  }
  return result
}

/** After saving, mark current as done and load next. Returns true if there's a next one. */
function loadNextParsed() {
  if (parsedCurrentIndex.value < 0 || parsedRecipes.value.length === 0) return false
  // Remove the just-saved recipe
  parsedRecipes.value.splice(parsedCurrentIndex.value, 1)
  if (parsedRecipes.value.length === 0) {
    parsedCurrentIndex.value = -1
    return false
  }
  // Load next (same index, or last if was at end)
  const next = Math.min(parsedCurrentIndex.value, parsedRecipes.value.length - 1)
  loadParsedIntoForm(next)
  return true
}

/** Sync current form edits back to parsedRecipes before switching */
function syncFormToParsed() {
  if (parsedCurrentIndex.value < 0) return
  const r = parsedRecipes.value[parsedCurrentIndex.value]
  if (!r) return
  r.name = formName.value
  // Rebuild ingredients from form (EO + coco)
  const ings = formIngredients.value.filter(i => i.oil && i.oil !== '椰子油').map(i => ({ oil: i.oil, drops: i.drops }))
  if (formCocoRow.value && cocoActualDrops.value > 0) {
    ings.push({ oil: '椰子油', drops: cocoActualDrops.value })
  }
  r.ingredients = ings
}

function loadParsedIntoForm(index) {
  // Save current edits before switching
  syncFormToParsed()
  const r = parsedRecipes.value[index]
  if (!r) return
  parsedCurrentIndex.value = index
  formName.value = r.name
  const cocoIng = r.ingredients.find(i => i.oil === '椰子油')
  const eoIngs = r.ingredients.filter(i => i.oil !== '椰子油')
  formIngredients.value = eoIngs.length > 0
    ? eoIngs.map(i => ({ ...i, _search: i.oil, _open: false }))
    : [{ oil: '', drops: 1, _search: '', _open: false }]
  if (cocoIng) {
    if (cocoIng._ml) {
      // Written as ml — use ml volume mode
      const mlStr = String(cocoIng._ml)
      const standardMls = ['5', '10', '15', '20', '30']
      formCocoRow.value = { oil: '椰子油', drops: 0, _search: '椰子油', _open: false }
      formVolume.value = standardMls.includes(mlStr) ? mlStr : 'custom'
      if (!standardMls.includes(mlStr)) formCustomVolume.value = cocoIng._ml
    } else {
      // Written as drops — use single mode
      formCocoRow.value = { oil: '椰子油', drops: cocoIng.drops, _search: '椰子油', _open: false }
      formVolume.value = 'single'
    }
  } else {
    formCocoRow.value = null
  }
  formNote.value = ''
  formTags.value = []
  if (!cocoIng) formVolume.value = ''
  if (r.notFound && r.notFound.length > 0) {
    ui.showToast(`未识别: ${r.notFound.join('、')}`)
  }
}

const sharedCount = ref({ adopted: 0, total: 0, adoptedNames: [], pendingNames: [] })

async function loadContribution() {
  try {
    const res = await api('/api/me/contribution')
    if (res.ok) {
      const data = await res.json()
      sharedCount.value = {
        adopted: data.adopted_count || 0,
        total: data.shared_count || 0,
        adoptedNames: data.adopted_names || [],
        pendingNames: data.pending_names || [],
      }
    }
  } catch {}
}
const previewRecipeIndex = ref(null)
const previewRecipeData = ref(null)
const showBatchMenu = ref(false)
const visibleAllTags = computed(() => {
  if (auth.canEdit) return recipeStore.allTags
  // Viewer: only show tags from their own diary recipes
  const myTags = new Set()
  for (const d of diaryStore.userDiary) {
    for (const t of (d.tags || [])) myTags.add(t)
  }
  return [...myTags].sort((a, b) => a.localeCompare(b, 'zh'))
})
const showBatchTagPicker = ref(false)
const batchTagsSelected = ref([])
const batchNewTag = ref('')
const batchTagsToRemove = ref([])

const batchExistingTags = computed(() => {
  const tagSets = []
  for (const id of selectedIds) {
    const r = recipeStore.recipes.find(x => x._id === id)
    if (r) tagSets.push(r.tags || [])
  }
  for (const id of selectedDiaryIds) {
    const d = diaryStore.userDiary.find(x => x.id === id)
    if (d) tagSets.push(d.tags || [])
  }
  if (!tagSets.length) return []
  const all = new Set(tagSets.flat())
  return [...all].sort((a, b) => a.localeCompare(b, 'zh'))
})
const totalSelected = computed(() => selectedIds.size + selectedDiaryIds.size)
const isMyAllSelected = computed(() => myFilteredRecipes.value.length > 0 && selectedDiaryIds.size === myFilteredRecipes.value.length)
const isPubAllSelected = computed(() => publicFilteredRecipes.value.length > 0 && selectedIds.size === publicFilteredRecipes.value.length)
const isAllSelected = computed(() => {
  const myOk = myFilteredRecipes.value.length > 0 && isMyAllSelected.value
  const pubOk = !auth.canEdit || (publicFilteredRecipes.value.length > 0 && isPubAllSelected.value)
  return myOk && pubOk
})
const showMyRecipes = ref(false)
const showPublicRecipes = ref(false)
const showReviewHistory = ref(false)
const reviewHistory = ref([])

function parseReviewDetail(detail) {
  try {
    const d = JSON.parse(detail)
    if (d.from_user) return `来自: ${d.from_user}`
    if (d.reason) return `原因: ${d.reason}`
  } catch {}
  return ''
}

function formatDate(d) {
  if (!d) return ''
  return new Date(d + 'Z').toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}

// Load diary on mount
onMounted(async () => {
  if (auth.isLoggedIn) {
    await diaryStore.loadDiary()
    await loadContribution()
  }
  if (auth.isAdmin) {
    try {
      const res = await api('/api/recipe-reviews')
      if (res.ok) reviewHistory.value = await res.json()
    } catch {}
    try {
      const res = await api('/api/users')
      if (res.ok) {
        const users = await res.json()
        seniorEditors.value = users.filter(u => u.role === 'senior_editor')
      }
    } catch {}
  }
  // Auto-expand pending if navigated from notification
  if (localStorage.getItem('oil_open_pending')) {
    localStorage.removeItem('oil_open_pending')
    showPending.value = true
  }
  // Open recipe editor if redirected from card view
  const editId = localStorage.getItem('oil_edit_recipe_id')
  if (editId) {
    localStorage.removeItem('oil_edit_recipe_id')
    const recipe = recipeStore.recipes.find(r => String(r._id) === editId)
    if (recipe) editRecipe(recipe)
  }
})

function editDiaryRecipe(diary) {
  editingRecipe.value = { _diary_id: diary.id, name: diary.name }
  formName.value = diary.name
  const ings = diary.ingredients || []
  formIngredients.value = ings.filter(i => i.oil !== '椰子油').map(i => ({ ...i, _search: i.oil, _open: false }))
  const coco = ings.find(i => i.oil === '椰子油')
  if (coco) {
    formCocoRow.value = { ...coco, _search: '椰子油', _open: false }
    const eoDrops = ings.filter(i => i.oil && i.oil !== '椰子油').reduce((s, i) => s + (i.drops || 0), 0)
    const totalDrops = eoDrops + coco.drops
    const ml = totalDrops / DROPS_PER_ML
    if (ml <= 2) formVolume.value = 'single'
    else if (Math.abs(ml - 5) < 1.5) formVolume.value = '5'
    else if (Math.abs(ml - 10) < 2.5) formVolume.value = '10'
    else if (Math.abs(ml - 15) < 2.5) formVolume.value = '15'
    else if (Math.abs(ml - 20) < 3) formVolume.value = '20'
    else if (Math.abs(ml - 30) < 6) formVolume.value = '30'
    else { formVolume.value = 'custom'; formCustomVolume.value = Math.round(ml) }
    if (eoDrops > 0 && coco.drops > 0) {
      const ratio = Math.round(coco.drops / eoDrops)
      const options = [3,4,5,6,7,8,9,10,12,15,20]
      formDilution.value = options.reduce((a, b) => Math.abs(b - ratio) < Math.abs(a - ratio) ? b : a)
    }
  } else {
    formCocoRow.value = null
    formVolume.value = ''
  }
  formNote.value = diary.note || ''
  formTags.value = [...(diary.tags || [])]
  showAddOverlay.value = true
}

async function assignReview(recipe) {
  const userId = recipe._assignTo
  if (!userId) return
  try {
    const res = await api('/api/recipes/' + recipe._id + '/assign-review', {
      method: 'POST',
      body: JSON.stringify({ user_id: userId }),
    })
    if (res.ok) {
      recipe._showAssign = false
      recipe._assignTo = ''
      ui.showToast('已指派审核')
    }
  } catch {
    ui.showToast('指派失败')
  }
}

function openRecipeDetail(recipe) {
  const idx = recipeStore.recipes.findIndex(r => r._id === recipe._id)
  if (idx >= 0) previewRecipeIndex.value = idx
}

function getVolumeLabel(ingredients) {
  const ings = ingredients || []
  const coco = ings.find(i => i.oil === '椰子油')
  if (!coco || !coco.drops) return ''
  const totalDrops = ings.reduce((s, i) => s + (i.drops || 0), 0)
  const ml = totalDrops / 18.6
  if (ml <= 2) return '单次'
  return `${Math.round(ml)}ml`
}

function diaryMatchesPublic(d) {
  const pub = recipeStore.recipes.find(r => r.name === d.name)
  if (!pub) return false
  const dIngs = (d.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
  const pIngs = (pub.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
  return dIngs.length === pIngs.length && dIngs.every((ing, i) => ing.oil === pIngs[i].oil && ing.drops === pIngs[i].drops)
}

function getDiaryShareStatus(d) {
  // Admin/senior_editor share directly — check public match first
  if (diaryMatchesPublic(d)) return 'shared'
  // Non-admin: check pending (owned by user, not yet adopted)
  if (!auth.isAdmin && !auth.canManage && sharedCount.value.pendingNames.includes(d.name)) return 'pending'
  return null
}

function handleShare(d) {
  const status = getDiaryShareStatus(d)
  if (status === 'shared') {
    ui.showToast('该配方已共享到公共配方库，感谢你的贡献！')
    return
  }
  if (status === 'pending') {
    ui.showToast('该配方正在审核中，请耐心等待')
    return
  }
  shareDiaryToPublic(d)
}

async function recommendApprove(recipe) {
  try {
    await api('/api/recipes/' + recipe._id + '/recommend', {
      method: 'POST',
      body: JSON.stringify({ recommendation: 'approve' }),
    })
    ui.showToast('已推荐通过，等待管理员最终审核')
  } catch {
    ui.showToast('操作失败')
  }
}

async function shareDiaryToPublic(diary) {
  const ings = (diary.ingredients || []).map(i => ({ oil: i.oil, drops: i.drops }))
  const result = await checkDupName(diary.name, ings, 'public')
  if (result === false) return
  if (result !== diary.name) diary = { ...diary, name: result }

  const ok = await showConfirm(`将「${diary.name}」共享到公共配方库？`)
  if (!ok) return
  try {
    const res = await api('/api/recipes', {
      method: 'POST',
      body: JSON.stringify({
        name: diary.name,
        note: diary.note || '',
        ingredients: (diary.ingredients || []).map(i => ({ oil_name: i.oil, drops: i.drops })),
        tags: diary.tags || [],
      }),
    })
    if (res.ok) {
      if (auth.isAdmin || auth.canManage) {
        ui.showToast('已共享到公共配方库')
      } else {
        ui.showToast('已提交，等待管理员审核')
      }
      await recipeStore.loadRecipes()
      await loadContribution()
    }
  } catch {
    ui.showToast('共享失败')
  }
}

async function removeDiaryRecipe(diary) {
  const ok = await showConfirm(`确定删除个人配方 "${diary.name}"？`)
  if (!ok) return
  try {
    await diaryStore.deleteDiary(diary.id)
    ui.showToast('已删除')
  } catch {
    ui.showToast('删除失败')
  }
}

async function removeRecipe(recipe) {
  const ok = await showConfirm(`确定删除配方 "${recipe.name}"？`)
  if (!ok) return
  try {
    await recipeStore.deleteRecipe(recipe._id)
    ui.showToast('已删除')
  } catch (e) {
    ui.showToast('删除失败')
  }
}

function recipesIdentical(a, b) {
  if ((a.ingredients || []).length !== (b.ingredients || []).length) return false
  const aIngs = [...a.ingredients].sort((x, y) => x.oil.localeCompare(y.oil))
  const bIngs = [...b.ingredients].sort((x, y) => x.oil.localeCompare(y.oil))
  return aIngs.every((ing, i) => ing.oil === bIngs[i].oil && ing.drops === bIngs[i].drops)
}

function formatIngsCompare(ings) {
  return (ings || []).map(i => `${i.oil} ${i.drops}滴`).join('、')
}

async function approveRecipe(recipe) {
  const dup = recipeStore.recipes.find(r => r.name === recipe.name && r._id !== recipe._id)
  if (dup) {
    if (recipesIdentical(recipe, dup)) {
      // Identical — delete this duplicate silently
      const ok = await showConfirm(`公共配方库中已有一模一样的配方「${recipe.name}」，忽略这条？`)
      if (!ok) return
      try {
        await api(`/api/recipes/${recipe._id}/reject`, { method: 'POST', body: '{}' })
        await recipeStore.loadRecipes()
        ui.showToast('已忽略重复配方')
      } catch { ui.showToast('操作失败') }
      return
    }
    // Different content
    const msg = `已有同名配方「${recipe.name}」但内容不同：\n\n` +
      `已有：${formatIngsCompare(dup.ingredients)}\n` +
      `新的：${formatIngsCompare(recipe.ingredients)}`
    const action = await showConfirm(msg, { okText: '改名后采纳', cancelText: '放弃' })
    if (!action) {
      // User chose to discard
      try {
        await api(`/api/recipes/${recipe._id}/reject`, { method: 'POST', body: JSON.stringify({ reason: '与已有同名配方重复' }) })
        await recipeStore.loadRecipes()
        ui.showToast('已放弃')
      } catch { ui.showToast('操作失败') }
      return
    }
    // User wants to rename
    const newName = await showPrompt('请输入新名称：', recipe.name)
    if (!newName || !newName.trim()) return
    try {
      await api(`/api/recipes/${recipe._id}`, {
        method: 'PUT',
        body: JSON.stringify({ name: newName.trim() }),
      })
      recipe.name = newName.trim()
    } catch {
      ui.showToast('改名失败')
      return
    }
  }
  try {
    const res = await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' })
    if (res.ok) {
      ui.showToast('已采纳并通知提交者')
      await recipeStore.loadRecipes()
    }
  } catch {
    ui.showToast('操作失败')
  }
}

async function rejectRecipe(recipe) {
  const reason = await showPrompt(`拒绝「${recipe.name}」的原因（选填）：`)
  if (reason === null) return
  try {
    const res = await api(`/api/recipes/${recipe._id}/reject`, {
      method: 'POST',
      body: JSON.stringify({ reason: reason || '' }),
    })
    if (res.ok) {
      await recipeStore.loadRecipes()
      ui.showToast('已拒绝并通知提交者')
    }
  } catch {
    ui.showToast('操作失败')
  }
}

async function exportExcel() {
  const recipes = recipeStore.recipes
  if (!recipes.length) { ui.showToast('没有配方可导出'); return }
  const XLSX = (await import('xlsx')).default || await import('xlsx')

  function recipesToRows(list) {
    return list.map(r => ({
      '配方名称': r.name,
      '标签': (r.tags || []).join('/'),
      '精油成分': r.ingredients.map(i => `${i.oil}${i.drops}滴`).join('、'),
      '成本': oils.fmtPrice(oils.calcCost(r.ingredients)),
      '备注': r.note || '',
    }))
  }

  const wb = XLSX.utils.book_new()
  // Sheet 1: 全部
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(recipesToRows(recipes)), '全部')
  // Per tag sheets
  const allTags = [...new Set(recipes.flatMap(r => r.tags || []))].sort((a, b) => a.localeCompare(b, 'zh'))
  for (const tag of allTags) {
    const tagged = recipes.filter(r => r.tags && r.tags.includes(tag))
    if (tagged.length) {
      const name = tag.substring(0, 31) // Excel sheet name max 31 chars
      XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(recipesToRows(tagged)), name)
    }
  }

  const today = new Date().toISOString().slice(0, 10)
  XLSX.writeFile(wb, `精油配方${today}.xlsx`)
  ui.showToast('导出成功')
}

function onTagPickerSave(tags) {
  formTags.value = tags
  showTagPicker.value = false
}

watch(() => recipeStore.recipes, () => {
  if (auth.isAdmin) {
    const pending = recipeStore.recipes
      .filter(r => r._owner_id && r._owner_id !== auth.user.id)
      .map(r => ({ ...r, _showAssign: false, _assignTo: '' }))
    pendingRecipes.value = pending
    pendingCount.value = pending.length
  }
}, { immediate: true })
</script>

<style scoped>
.recipe-manager {
  padding: 0 12px 24px;
}

.login-prompt {
  text-align: center; padding: 60px 20px; color: #6b6375;
}
.login-prompt p { margin-bottom: 16px; font-size: 15px; }

.review-bar {
  background: linear-gradient(135deg, #fff3e0, #ffe0b2);
  padding: 12px 16px;
  border-radius: 10px;
  margin-bottom: 12px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: #e65100;
}

.pending-list {
  margin-bottom: 12px;
}

.pending-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: #fffde7;
  border-radius: 8px;
  margin-bottom: 6px;
  font-size: 13px;
}

.pending-name {
  font-weight: 600;
  flex: 1;
}
.pending-name.clickable {
  cursor: pointer;
  color: #4a9d7e;
  text-decoration: underline;
}
.pending-name.clickable:hover {
  color: #2e7d5a;
}

.pending-owner {
  color: #999;
  font-size: 12px;
}

.manage-toolbar {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

.search-box {
  display: flex;
  align-items: center;
  background: #f8f7f5;
  border-radius: 10px;
  padding: 2px 8px;
  border: 1.5px solid #e5e4e7;
  flex: 1;
  min-width: 160px;
}

.search-input {
  flex: 1;
  border: none;
  background: transparent;
  padding: 8px 6px;
  font-size: 14px;
  outline: none;
  font-family: inherit;
}

.search-clear-btn {
  border: none;
  background: transparent;
  cursor: pointer;
  color: #999;
  padding: 4px;
}

.tag-filter-bar {
  margin-bottom: 12px;
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.tag-toggle-btn {
  background: #f8f7f5;
  border: 1.5px solid #e5e4e7;
  border-radius: 10px;
  padding: 8px 16px;
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
  color: #3e3a44;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.tag-chip {
  padding: 4px 12px;
  border-radius: 16px;
  background: #f0eeeb;
  font-size: 12px;
  cursor: pointer;
  color: #6b6375;
  border: 1.5px solid transparent;
  transition: all 0.15s;
}

.tag-chip.active {
  background: #e8f5e9;
  border-color: #7ec6a4;
  color: #2e7d5a;
  font-weight: 600;
}

.batch-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: #e8f5e9;
  border-radius: 10px;
  margin-bottom: 12px;
  font-size: 13px;
  flex-wrap: wrap;
}

.batch-select {
  padding: 6px 10px;
  border-radius: 8px;
  border: 1.5px solid #d4cfc7;
  font-size: 13px;
  font-family: inherit;
  background: #fff;
}

.recipe-section {
  margin-bottom: 20px;
}

.section-title {
  font-size: 15px;
  font-weight: 600;
  color: #3e3a44;
  margin: 0 0 10px;
}

.recipe-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.recipe-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: #fff;
  border: 1.5px solid #e5e4e7;
  border-radius: 10px;
  transition: all 0.15s;
}

.recipe-row:hover {
  border-color: #d4cfc7;
  background: #fafaf8;
}

.recipe-row.selected {
  border-color: #7ec6a4;
  background: #f0faf5;
}

.row-check {
  width: 16px;
  height: 16px;
  cursor: pointer;
  accent-color: #4a9d7e;
}

.row-info {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  min-width: 0;
  flex-wrap: wrap;
}

.row-name {
  font-weight: 600;
  font-size: 14px;
  color: #3e3a44;
}

.row-owner {
  font-size: 11px;
  color: #b0aab5;
}

.row-tags {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}

.mini-tag {
  padding: 2px 8px;
  border-radius: 10px;
  background: #f0eeeb;
  font-size: 11px;
  color: #6b6375;
}

.share-tag {
  font-size: 11px;
  padding: 1px 8px;
  border-radius: 8px;
  font-weight: 500;
  white-space: nowrap;
}
.share-tag.shared { background: #e8f5e9; color: #2e7d32; }
.share-tag.pending { background: #fff3e0; color: #e65100; }
.row-volume { font-size: 10px; color: #b0aab5; white-space: nowrap; }

.review-history { max-height: 300px; overflow-y: auto; }
.review-log-item {
  display: flex; align-items: center; gap: 8px; padding: 8px 12px;
  border-bottom: 1px solid #f5f5f5; font-size: 13px; flex-wrap: wrap;
}
.log-approve { color: #2e7d32; font-weight: 600; white-space: nowrap; }
.log-reject { color: #c62828; font-weight: 600; white-space: nowrap; }
.log-recipe { font-weight: 500; color: #3e3a44; }
.log-from { color: #999; font-size: 12px; }
.log-time { color: #bbb; font-size: 11px; margin-left: auto; white-space: nowrap; }
.section-title.clickable { cursor: pointer; display: flex; align-items: center; gap: 6px; }
.toggle-icon { font-size: 12px; color: #999; margin-left: auto; }

.contrib-tag {
  font-size: 11px;
  color: #4a9d7e;
  background: #e8f5e9;
  padding: 2px 8px;
  border-radius: 8px;
  font-weight: 500;
}

.row-cost {
  font-size: 13px;
  color: #4a9d7e;
  font-weight: 500;
  margin-left: auto;
  white-space: nowrap;
}

.row-actions {
  display: flex;
  gap: 4px;
}

.btn-icon {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 16px;
  padding: 4px;
  border-radius: 6px;
}

.btn-icon:hover {
  background: #f0eeeb;
}

/* Overlay */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

.overlay-panel {
  background: #fff;
  border-radius: 16px;
  padding: 24px;
  width: 100%;
  max-width: 520px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
}

.overlay-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 18px;
}

.overlay-header h3 {
  margin: 0;
  font-size: 17px;
  color: #3e3a44;
}

.btn-close {
  border: none;
  background: #f0eeeb;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #6b6375;
}

.paste-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}

.paste-input {
  width: 100%;
  border: 1.5px solid #d4cfc7;
  border-radius: 10px;
  padding: 10px 14px;
  font-size: 13px;
  font-family: inherit;
  resize: vertical;
  outline: none;
  box-sizing: border-box;
}

.paste-input:focus {
  border-color: #7ec6a4;
}

.parsed-queue { margin: 12px 0; background: #f8faf8; border: 1.5px solid #d4e8d4; border-radius: 10px; padding: 10px 12px; }
.parsed-queue-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.parsed-queue-label { font-size: 13px; font-weight: 600; color: #2e7d5a; flex: 1; }
.parsed-queue-list { display: flex; flex-wrap: wrap; gap: 6px; }
.parsed-queue-item {
  display: flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 8px;
  border: 1.5px solid #e5e4e7; background: #fff; font-size: 12px; cursor: pointer; font-family: inherit;
}
.parsed-queue-item.active { border-color: #7ec6a4; background: #e8f5e9; font-weight: 600; }
.parsed-queue-item:hover { border-color: #d4cfc7; }
.parsed-queue-name { color: #3e3a44; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.parsed-queue-count { color: #b0aab5; font-size: 11px; }

.editor-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.editor-name-input { width: 100%; font-size: 17px; font-weight: 600; border: none; border-bottom: 2px solid #e5e4e7; padding: 6px 0; outline: none; font-family: inherit; background: transparent; color: #3e3a44; }
.editor-name-input::placeholder { color: #ccc; font-weight: 400; }
.editor-name-input:focus { border-bottom-color: #7ec6a4; }
.editor-header-actions { display: flex; gap: 6px; flex-shrink: 0; }
.editor-tip { font-size: 12px; color: #999; background: #f8f7f5; padding: 8px 12px; border-radius: 8px; margin-bottom: 12px; }
.editor-section { margin-bottom: 16px; }
.editor-label { font-size: 13px; font-weight: 600; color: #3e3a44; margin-bottom: 6px; display: block; }
.editor-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 8px; }
.editor-table th { text-align: left; padding: 6px 4px; color: #999; font-weight: 500; font-size: 12px; border-bottom: 1px solid #eee; }
.editor-table td { padding: 6px 4px; border-bottom: 1px solid #f5f5f5; }
.editor-drops { width: 65px; padding: 6px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; text-align: center; outline: none; font-family: inherit; }
.editor-drops:focus { border-color: #7ec6a4; }
.editor-input { padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; outline: none; font-family: inherit; width: 100%; box-sizing: border-box; }
.editor-input:focus { border-color: #7ec6a4; }
.editor-textarea { width: 100%; padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; font-family: inherit; outline: none; resize: vertical; box-sizing: border-box; }
.editor-textarea:focus { border-color: #7ec6a4; }
.ing-ppd { color: #b0aab5; font-size: 12px; }
.ing-cost { color: #4a9d7e; font-weight: 500; font-size: 13px; }
.remove-row-btn { border: none; background: none; color: #ccc; cursor: pointer; font-size: 16px; padding: 2px 4px; }
.remove-row-btn:hover { color: #c0392b; }
.add-row-btn { border: 1.5px dashed #d4cfc7; background: none; color: #999; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; width: 100%; }
.add-row-btn:hover { border-color: #7ec6a4; color: #4a9d7e; }
.add-ingredient-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
.editor-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.editor-tag { background: #e8f5e9; color: #2e7d5a; padding: 4px 10px; border-radius: 12px; font-size: 12px; display: flex; align-items: center; gap: 4px; }
.tag-remove { cursor: pointer; font-size: 14px; color: #999; }
.tag-remove:hover { color: #c0392b; }
.candidate-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.candidate-tag { background: #f0eeeb; color: #6b6375; padding: 4px 10px; border-radius: 12px; font-size: 12px; cursor: pointer; }
.candidate-tag:hover { background: #e8f5e9; color: #2e7d5a; }
.tag-input-row { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
.editor-total { text-align: right; font-size: 15px; font-weight: 600; color: #4a9d7e; padding: 10px 0; border-top: 1px solid #eee; }
.action-btn { border: 1.5px solid #d4cfc7; background: #fff; color: #6b6375; border-radius: 8px; padding: 6px 14px; font-size: 13px; cursor: pointer; font-family: inherit; white-space: nowrap; }
.action-btn:hover { background: #f8f7f5; }
.action-btn-primary { background: linear-gradient(135deg, #7ec6a4, #4a9d7e); color: #fff; border-color: transparent; }
.action-btn-primary:hover { opacity: 0.9; }
.action-btn-sm { padding: 5px 12px; font-size: 12px; }
.volume-controls { display: flex; gap: 4px; margin-bottom: 8px; flex-wrap: wrap; }
.volume-btn { padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; background: #fff; font-size: 13px; cursor: pointer; font-family: inherit; color: #6b6375; text-align: center; }
.volume-btn.active { background: #e8f5e9; border-color: #7ec6a4; color: #2e7d5a; font-weight: 600; }
.volume-btn:hover { border-color: #7ec6a4; }
.custom-volume-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.ratio-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.ratio-label { font-size: 12px; color: #4a9d7e; font-weight: 500; white-space: nowrap; }
.ratio-options { display: flex; gap: 4px; flex-wrap: wrap; }
.ratio-btn {
  padding: 3px 8px; border: 1px solid #e5e4e7; border-radius: 6px; background: #fff;
  font-size: 11px; cursor: pointer; font-family: inherit; color: #6b6375; min-width: 28px; text-align: center;
}
.ratio-btn.active { background: #e8f5e9; border-color: #7ec6a4; color: #2e7d5a; font-weight: 600; }
.ratio-btn:hover { border-color: #7ec6a4; }
.ratio-hint { font-size: 12px; color: #4a9d7e; font-weight: 500; white-space: nowrap; }

.coco-row { background: #f8faf8; }
.coco-label { font-weight: 600; color: #4a9d7e; font-size: 13px; }
.coco-fill { font-size: 12px; color: #4a9d7e; font-weight: 500; }
.recipe-summary {
  padding: 10px 14px; background: #f0faf5; border-radius: 10px; border-left: 3px solid #7ec6a4;
  font-size: 13px; color: #2e7d5a; margin-bottom: 12px; line-height: 1.6;
}
.drops-sm { width: 50px; padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; text-align: center; outline: none; font-family: inherit; }
.drops-sm:focus { border-color: #7ec6a4; }
.select-sm { padding: 3px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; background: #fff; width: auto; }
.btn-select-active { background: #e8f5e9; color: #2e7d5a; border: 1.5px solid #7ec6a4; border-radius: 10px; padding: 7px 14px; font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 600; }
.search-bar { margin-bottom: 8px; }
.search-bar .search-box {
  display: flex; align-items: center; background: #f8f7f5; border: 1.5px solid #e5e4e7;
  border-radius: 10px; padding: 2px 10px;
}
.search-bar .search-input {
  flex: 1; border: none; background: transparent; padding: 8px 4px; font-size: 14px;
  outline: none; font-family: inherit;
}

.action-bar {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-bottom: 10px;
}
.action-chip {
  padding: 5px 14px; border: 1.5px solid #e5e4e7; border-radius: 20px; background: #fff;
  font-size: 13px; cursor: pointer; font-family: inherit; color: #6b6375; white-space: nowrap;
  transition: all 0.15s;
}
.action-chip:hover { border-color: #7ec6a4; color: #4a9d7e; }
.action-chip.active { background: #e8f5e9; border-color: #7ec6a4; color: #2e7d5a; font-weight: 600; }
.action-chip.cancel { color: #999; }
.chip-count {
  display: inline-block; background: #4a9d7e; color: #fff; font-size: 11px;
  min-width: 16px; height: 16px; line-height: 16px; text-align: center;
  border-radius: 8px; margin-left: 4px; padding: 0 4px;
}
.detail-close-btn {
  border: none; background: #f0eeeb; width: 26px; height: 26px; border-radius: 50%;
  cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; color: #6b6375;
}
.select-count { font-size: 12px; color: #4a9d7e; font-weight: 500; white-space: nowrap; }
.batch-menu { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.batch-menu-btn {
  padding: 5px 12px; border: 1.5px solid #e5e4e7; border-radius: 8px; background: #fff;
  font-size: 12px; cursor: pointer; font-family: inherit; color: #6b6375;
}
.batch-menu-btn:hover { border-color: #7ec6a4; color: #4a9d7e; }
.batch-delete { color: #c0392b; border-color: #e8b4b0; }
.batch-delete:hover { background: #fdf0ee; border-color: #c0392b; }
.batch-tag-picker {
  padding: 12px; background: #f8faf8; border: 1.5px solid #d4e8d4; border-radius: 10px; margin-bottom: 10px;
}
.tag-removable { cursor: pointer; transition: all 0.15s; }
.tag-removable:hover { background: #fce4ec; border-color: #e8b4b0; color: #c62828; }
.tag-marked-remove { background: #ffebee !important; color: #c62828 !important; text-decoration: line-through; }
.mini-select {
  width: 18px; height: 18px; border: 1.5px solid #d4cfc7; border-radius: 4px;
  background: #fff; color: transparent; font-size: 11px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  padding: 0; margin-right: 4px; line-height: 1;
}
.mini-select.active { background: #4a9d7e; border-color: #4a9d7e; color: #fff; }
.assign-row { display: flex; gap: 6px; align-items: center; margin-top: 4px; }
.assign-select { padding: 4px 8px; border: 1px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; }
.btn-primary { background: linear-gradient(135deg, #7ec6a4, #4a9d7e); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-family: inherit; }
.tag-list-bar {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; padding: 8px 0;
}
.tag-delete { margin-left: 4px; cursor: pointer; font-size: 12px; color: #ccc; }
.tag-delete:hover { color: #c0392b; }
.tag-add-row { display: inline-flex; gap: 4px; align-items: center; }
.tag-add-input { width: 80px; padding: 4px 8px; border: 1px solid #e5e4e7; border-radius: 8px; font-size: 12px; outline: none; font-family: inherit; }
.tag-add-input:focus { border-color: #7ec6a4; }
.tag-add-btn { border: none; background: #e8f5e9; color: #4a9d7e; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; font-family: inherit; }
.tag-add-btn:disabled { opacity: 0.4; }

.export-btn {
  margin-left: auto;
  border: none; background: none; cursor: pointer; font-size: 16px; padding: 4px 6px;
  opacity: 0.6;
}
.export-btn:hover { opacity: 1; }
.batch-count { font-size: 12px; color: #4a9d7e; font-weight: 600; white-space: nowrap; }
.batch-select { padding: 5px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 12px; font-family: inherit; background: #fff; }

.divider-text {
  text-align: center;
  color: #b0aab5;
  font-size: 12px;
  margin: 12px 0;
  position: relative;
}

.divider-text::before,
.divider-text::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 35%;
  height: 1px;
  background: #e5e4e7;
}

.divider-text::before {
  left: 0;
}

.divider-text::after {
  right: 0;
}

.form-group {
  margin-bottom: 14px;
}

.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: #3e3a44;
  margin-bottom: 6px;
}

.form-input {
  width: 100%;
  padding: 10px 14px;
  border: 1.5px solid #d4cfc7;
  border-radius: 10px;
  font-size: 14px;
  font-family: inherit;
  outline: none;
  box-sizing: border-box;
}

.form-input:focus {
  border-color: #7ec6a4;
}

.ing-row {
  display: flex;
  gap: 6px;
  align-items: center;
  margin-bottom: 6px;
}

.form-select {
  flex: 1;
  padding: 8px 10px;
  border: 1.5px solid #d4cfc7;
  border-radius: 8px;
  font-size: 13px;
  font-family: inherit;
  background: #fff;
}

.form-input-sm {
  width: 70px;
  padding: 8px 10px;
  border: 1.5px solid #d4cfc7;
  border-radius: 8px;
  font-size: 13px;
  font-family: inherit;
  outline: none;
  text-align: center;
}

.oil-search-wrap {
  flex: 1;
  position: relative;
}

.oil-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: #fff;
  border: 1.5px solid #d4cfc7;
  border-radius: 8px;
  max-height: 180px;
  overflow-y: auto;
  z-index: 10;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.oil-option {
  padding: 8px 12px;
  font-size: 13px;
  cursor: pointer;
}

.oil-option:hover {
  background: #e8f5e9;
}

.oil-empty {
  color: #999;
  cursor: default;
}

.oil-empty:hover {
  background: transparent;
}

.btn-icon-sm {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 14px;
  color: #999;
  padding: 4px;
}

.overlay-footer {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  margin-top: 18px;
}

/* Buttons */
.btn-primary {
  background: linear-gradient(135deg, #7ec6a4 0%, #4a9d7e 100%);
  color: #fff;
  border: none;
  border-radius: 10px;
  padding: 9px 20px;
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
  white-space: nowrap;
}

.btn-primary:hover {
  opacity: 0.9;
}

.btn-primary:disabled {
  opacity: 0.5;
  cursor: default;
}

.btn-outline {
  background: #fff;
  color: #6b6375;
  border: 1.5px solid #d4cfc7;
  border-radius: 10px;
  padding: 9px 20px;
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
  white-space: nowrap;
}

.btn-outline:hover {
  background: #f8f7f5;
}

.toolbar-actions {
  display: flex;
  gap: 8px;
}

.btn-sm {
  padding: 7px 14px;
  font-size: 13px;
}

.btn-danger-outline {
  background: #fff;
  color: #c0392b;
  border: 1.5px solid #e8b4b0;
  border-radius: 10px;
  padding: 7px 14px;
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
}

.btn-danger-outline:hover {
  background: #fdf0ee;
}

.btn-sm {
  padding: 6px 14px;
  font-size: 12px;
  border-radius: 8px;
}

.btn-approve {
  background: #4a9d7e;
  color: #fff;
  border: none;
  cursor: pointer;
  font-family: inherit;
}

.btn-reject {
  background: #ef5350;
  color: #fff;
  border: none;
  cursor: pointer;
  font-family: inherit;
}

.empty-hint {
  text-align: center;
  color: #b0aab5;
  font-size: 13px;
  padding: 24px 0;
}

.toggle-icon {
  font-size: 12px;
}

@media (max-width: 600px) {
  .manage-toolbar {
    flex-direction: column;
  }
}
</style>
