<template>
  <q-page class="q-pa-xs pricing-rules-page">
    <div class="top-bar row items-center justify-between q-mb-xs">
      <div>
        <div class="text-subtitle1 text-weight-bold">Fiyat Carpani Kurallari</div>
      </div>
      <q-btn
        flat
        color="primary"
        icon="refresh"
        label="Yenile"
        :loading="loading"
        @click="refreshRows"
      />
    </div>

    <div class="action-bar row items-center justify-between q-mb-xs">
      <div class="text-caption text-grey-8">
        Satir: {{ filteredRows.length }} / {{ rows.length }} | Degisen: {{ selectedDirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ selectedCount }}
      </div>
      <div class="row items-center q-gutter-sm">
        <div class="row items-center q-gutter-xs action-legend">
          <q-chip dense square color="orange-1" text-color="deep-orange-8" icon="content_copy">
            Kopya secimi
          </q-chip>
          <q-chip dense square color="light-green-1" text-color="green-9" icon="task_alt">
            Kaydetme secimi
          </q-chip>
        </div>
        <q-btn
          flat
          color="primary"
          icon="filter_alt_off"
          label="Filtreleri Temizle"
          :disable="!hasAnyFilter"
          @click="clearAllFilters"
        />
        <q-btn
          color="secondary"
          unelevated
          icon="content_copy"
          label="Kopyala"
          :disable="!canCopySelected"
          @click="copySelectedToSelected"
        />
        <q-btn
          color="primary"
          flat
          icon="download"
          label="Sayfayi Excel'e Aktar"
          :disable="filteredRows.length === 0"
          @click="exportCurrentView"
        />
        <q-btn
          color="primary"
          outline
          icon="download_for_offline"
          label="Tum Filtreyi Excel'e Aktar"
          :disable="rows.length === 0"
          @click="exportAllFiltered"
        />
        <q-btn
          color="primary"
          outline
          icon="upload_file"
          label="Verileri CSV'den Yukle"
          :disable="loading || rows.length === 0"
          @click="openImportDialog"
        />
        <q-btn
          color="primary"
          unelevated
          icon="save"
          :disable="!canUpdate || selectedDirtyCount === 0 || saving"
          :loading="saving"
          :label="`Kaydet (${selectedDirtyCount})`"
          @click="saveSelected"
        />
        <input
          ref="fileInputRef"
          type="file"
          accept=".csv,text/csv"
          class="hidden-file-input"
          @change="onImportFileChange"
        >
      </div>
    </div>

    <q-banner
      v-if="csvImportStatus"
      dense
      class="q-mb-xs"
      :class="csvImportStatus.type === 'warning' ? 'bg-amber-2 text-amber-10' : 'bg-green-1 text-green-10'"
    >
      {{ csvImportStatus.message }}
    </q-banner>

    <div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
      <q-table
        flat
        dense
        row-key="_row_key"
        :rows="filteredRows"
        :columns="columns"
        :loading="loading"
        :rows-per-page-options="[0]"
        v-model:pagination="tablePagination"
        binary-state-sort
        hide-bottom
        :table-style="tableStyle"
        class="pane-table rules-table"
      >
        <template #no-data>
          <div class="full-width row flex-center q-pa-lg text-grey-7">
            Parametre cache'i henuz dolmadi veya aktif filtrelerle eslesen kayit bulunamadi.
          </div>
        </template>

        <template #header="props">
          <q-tr :props="props" class="header-row-fixed">
            <q-th
              v-for="col in props.cols"
              :key="col.name"
              :props="props"
              :class="[col.headerClasses, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
              :style="getHeaderCellStyle(col)"
            >
              <q-checkbox
                v-if="col.name === 'select'"
                dense
                size="24px"
                class="rule-select-checkbox"
                color="positive"
                :model-value="allSelectedVisible"
                :indeterminate="someSelectedVisible && !allSelectedVisible"
                @update:model-value="toggleSelectAllVisible"
              />
              <div v-else-if="col.name === 'copy_select'" class="selection-header-copy">
                <q-icon name="content_copy" size="16px" />
                <span>Kopya</span>
              </div>
              <div v-else class="header-with-filter">
                <span>{{ col.label }}</span>
                <q-btn
                  v-if="isHeaderFilterField(col.field)"
                  dense
                  flat
                  round
                  size="8px"
                  icon="filter_alt"
                  :color="hasFilter(col.field) ? 'primary' : 'grey-7'"
                  class="header-filter-btn"
                  @click.stop
                >
                  <q-badge
                    v-if="hasFilter(col.field)"
                    floating
                    rounded
                    color="primary"
                    :label="getFilterBadgeValue(col.field)"
                  />
                  <q-menu anchor="bottom right" self="top right" @click.stop>
                    <div v-if="isMultiSelectFilterField(col.field)" class="excel-filter-menu">
                      <q-input
                        v-model="columnFilterSearch[col.field]"
                        dense
                        outlined
                        clearable
                        debounce="150"
                        placeholder="Ara"
                      />
                      <div class="excel-filter-actions row items-center justify-between q-mt-xs">
                        <q-btn flat dense size="sm" color="primary" label="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
                        <q-btn flat dense size="sm" color="grey-8" label="Temizle" @click="clearColumnFilter(col.field)" />
                      </div>
                      <q-virtual-scroll
                        v-if="getFilterOptionsForField(col.field).length > 0"
                        class="excel-filter-options"
                        :items="getFilterOptionsForField(col.field)"
                        :virtual-scroll-item-size="32"
                        separator
                      >
                        <template #default="{ item: option }">
                          <q-item
                            dense
                            clickable
                            class="excel-filter-option"
                            @click="toggleColumnFilterValue(col.field, option.value)"
                          >
                            <q-item-section avatar>
                              <q-checkbox
                                dense
                                :model-value="isColumnFilterValueSelected(col.field, option.value)"
                                @click.stop
                                @update:model-value="toggleColumnFilterValue(col.field, option.value)"
                              />
                            </q-item-section>
                            <q-item-section>
                              <q-item-label>{{ option.label }}</q-item-label>
                            </q-item-section>
                          </q-item>
                        </template>
                      </q-virtual-scroll>
                      <div v-else class="excel-filter-empty">Sonuc yok</div>
                    </div>
                    <div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
                      <div class="range-filter-grid">
                        <q-input
                          dense
                          outlined
                          type="number"
                          label="Minimum"
                          :model-value="numberRangeFilters[col.field]?.min"
                          @update:model-value="(value) => updateNumberRangeFilter(col.field, 'min', value)"
                        />
                        <q-input
                          dense
                          outlined
                          type="number"
                          label="Maksimum"
                          :model-value="numberRangeFilters[col.field]?.max"
                          @update:model-value="(value) => updateNumberRangeFilter(col.field, 'max', value)"
                        />
                      </div>
                      <div class="excel-filter-actions row items-center justify-end q-mt-xs">
                        <q-btn flat dense size="sm" color="grey-8" label="Temizle" @click="clearRangeFilter(col.field)" />
                      </div>
                    </div>
                  </q-menu>
                </q-btn>
                <span v-else class="header-filter-ghost" />
              </div>
            </q-th>
          </q-tr>
        </template>

        <template #body="props">
          <q-tr :props="props">
            <q-td
              v-for="col in props.cols"
              :key="col.name"
              :props="props"
              :class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
              :style="getBodyCellStyle(col)"
            >
              <div
                v-if="col.name === 'copy_select'"
                class="row items-center no-wrap justify-center copy-cell-wrap"
              >
                <q-checkbox
                  dense
                  size="24px"
                  class="rule-select-checkbox"
                  color="secondary"
                  :model-value="isCopySelected(props.row)"
                  @update:model-value="(value) => toggleCopySelected(props.row, value)"
                />
                <q-badge
                  v-if="copyRoleLabel(props.row)"
                  color="deep-orange"
                  class="q-ml-xs"
                >
                  {{ copyRoleLabel(props.row) }}
                </q-badge>
              </div>
              <q-checkbox
                v-else-if="col.name === 'select'"
                dense
                size="24px"
                class="rule-select-checkbox"
                color="positive"
                :model-value="isRowSelected(props.row)"
                @update:model-value="(value) => setRowSelected(props.row, value)"
              />
              <q-badge v-else-if="col.name === 'has_rule'" :color="props.row.has_rule ? 'positive' : 'grey-6'">
                {{ props.row.has_rule ? 'Tanimli' : 'Yeni' }}
              </q-badge>
              <q-toggle
                v-else-if="col.name === 'is_active'"
                v-model="props.row.is_active"
                dense
                @update:model-value="() => markDirty(props.row)"
              />
              <q-toggle
                v-else-if="col.name === 'calc_enabled'"
                v-model="props.row.calc_enabled"
                dense
                @update:model-value="() => markDirty(props.row)"
              />
              <q-toggle
                v-else-if="col.name === 'publish_postgres'"
                v-model="props.row.publish_postgres"
                dense
                @update:model-value="() => markDirty(props.row)"
              />
              <q-toggle
                v-else-if="col.name === 'publish_nebim'"
                v-model="props.row.publish_nebim"
                dense
                @update:model-value="() => markDirty(props.row)"
              />
              <q-select
                v-else-if="retailModeFields.has(col.name)"
                dense
                outlined
                emit-value
                map-options
                :options="retailModeOptions.map(value => ({ label: value, value }))"
                :model-value="props.row[col.field]"
                @update:model-value="(value) => updateRetailMode(props.row, col.field, value)"
              />
              <input
                v-else-if="numericFields.has(col.name)"
                class="native-cell-input text-right"
                inputmode="decimal"
                :value="props.row[col.field]"
                @input="(event) => updateNumber(props.row, col.field, event.target.value)"
              >
              <span v-else class="cell-text" :title="String(col.value ?? '')">
                {{ col.value }}
              </span>
            </q-td>
          </q-tr>
        </template>
      </q-table>
    </div>

    <q-inner-loading :showing="saving" label="Kaydediliyor..." />
  </q-page>
</template>

<script setup>
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { Dialog, Notify } from 'quasar'
import api, { download } from 'src/services/api'
import { usePermissionStore } from 'stores/permissionStore'

const perm = usePermissionStore()
const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))

const loading = ref(false)
const saving = ref(false)
const rows = ref([])
const fileInputRef = ref(null)
const selectedKeyMap = ref({})
const copySelectedKeys = ref([])
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
const csvImportStatus = ref(null) // { type: 'positive'|'warning', message: string, at: string }
let emptyRetryTimer = null

const numericFields = new Set([
  'try_base', 'try1', 'try2', 'try3', 'try4', 'try5', 'try6', 'try_wholesale_step', 'try_retail_step',
  'usd_base', 'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6', 'usd_wholesale_step', 'usd_retail_step',
  'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_wholesale_step', 'eur_retail_step'
])
const retailModeFields = new Set(['try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'])
const retailModeOptions = ['STEP', 'END_99', 'END_49', 'BAND_99', 'BAND_49']

const importKeyFieldLabels = [
  ['askili_yan', 'ASKILI YAN'],
  ['kategori', 'KATEGORI'],
  ['urun_ilk_grubu', 'URUN ILK GRUBU'],
  ['urun_ana_grubu', 'URUN ANA GRUBU'],
  ['urun_alt_grubu', 'URUN ALT GRUBU'],
  ['icerik', 'ICERIK'],
  ['marka', 'MARKA'],
  ['brand_code', 'BRAND CODE'],
  ['brand_group', 'MARKA GRUBU']
]

const importFieldMap = {
  AKTIF: 'is_active',
  'HESAP AKTIF': 'calc_enabled',
  'PG YAYIN': 'publish_postgres',
  'NEBIM YAYIN': 'publish_nebim',
  'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
  'TRY PERAKENDE MODU': 'try_retail_mode',
  'TRY PERAKENDE DEGERI': 'try_retail_step',
  'TRY PERAKENDE YUVARLAMA': 'try_retail_step',
  'TRY YUVARLAMA': 'try_wholesale_step',
  'TRY TABAN': 'try_base',
  'TRY 1': 'try1',
  'TRY 2': 'try2',
  'TRY 3': 'try3',
  'TRY 4': 'try4',
  'TRY 5': 'try5',
  'TRY 6': 'try6',
  'USD TOPTAN YUVARLAMA': 'usd_wholesale_step',
  'USD PERAKENDE MODU': 'usd_retail_mode',
  'USD PERAKENDE DEGERI': 'usd_retail_step',
  'USD PERAKENDE YUVARLAMA': 'usd_retail_step',
  'USD YUVARLAMA': 'usd_wholesale_step',
  'USD TABAN': 'usd_base',
  'USD 1': 'usd1',
  'USD 2': 'usd2',
  'USD 3': 'usd3',
  'USD 4': 'usd4',
  'USD 5': 'usd5',
  'USD 6': 'usd6',
  'EUR TOPTAN YUVARLAMA': 'eur_wholesale_step',
  'EUR PERAKENDE MODU': 'eur_retail_mode',
  'EUR PERAKENDE DEGERI': 'eur_retail_step',
  'EUR PERAKENDE YUVARLAMA': 'eur_retail_step',
  'EUR YUVARLAMA': 'eur_wholesale_step',
  'EUR TABAN': 'eur_base',
  'EUR 1': 'eur1',
  'EUR 2': 'eur2',
  'EUR 3': 'eur3',
  'EUR 4': 'eur4',
  'EUR 5': 'eur5',
  'EUR 6': 'eur6'
}

const multiFilterFields = [
  'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
  'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
  'try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'
]
const multiSelectFilterFieldSet = new Set(multiFilterFields)
const numberRangeFilterFieldSet = new Set(numericFields)
const headerFilterFieldSet = new Set([...multiFilterFields, ...numericFields])

const columnFilters = ref(Object.fromEntries(multiFilterFields.map(field => [field, []])))
const columnFilterSearch = ref(Object.fromEntries(multiFilterFields.map(field => [field, ''])))
const numberRangeFilters = ref(Object.fromEntries([...numericFields].map(field => [field, { min: '', max: '' }])))

function col (name, label, field, width, extra = {}) {
  const size = `width:${width}px;min-width:${width}px;max-width:${width}px;`
  return {
    name,
    label,
    field,
    sortable: name !== 'select',
    align: 'left',
    style: size,
    headerStyle: size,
    ...extra
  }
}

const columns = [
  col('copy_select', 'KOPYA', 'copy_select', 68, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
  col('select', 'KAYDET', 'select', 58, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
  col('has_rule', 'DURUM', 'has_rule', 54, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('is_active', 'AKTIF', 'is_active', 42, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('askili_yan', 'ASKILI YAN', 'askili_yan', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('kategori', 'KATEGORI', 'kategori', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('urun_ilk_grubu', 'URUN ILK GRUBU', 'urun_ilk_grubu', 84, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('urun_ana_grubu', 'URUN ANA GRUBU', 'urun_ana_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('urun_alt_grubu', 'URUN ALT GRUBU', 'urun_alt_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('icerik', 'ICERIK', 'icerik', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('marka', 'MARKA', 'marka', 80, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('brand_code', 'BRAND CODE', 'brand_code', 68, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('publish_postgres', 'PG YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
  col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),

  col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try_retail_mode', 'TRY PERAKENDE MODU', 'try_retail_mode', 76, { classes: 'try-col', headerClasses: 'try-col' }),
  col('try_retail_step', 'TRY PERAKENDE DEGERI', 'try_retail_step', 78, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try_base', 'TRY TABAN', 'try_base', 58, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try1', 'TRY 1', 'try1', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try2', 'TRY 2', 'try2', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try3', 'TRY 3', 'try3', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try4', 'TRY 4', 'try4', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try5', 'TRY 5', 'try5', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
  col('try6', 'TRY 6', 'try6', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),

  col('usd_wholesale_step', 'USD TOPTAN YUVARLAMA', 'usd_wholesale_step', 76, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd_retail_mode', 'USD PERAKENDE MODU', 'usd_retail_mode', 76, { classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd_retail_step', 'USD PERAKENDE DEGERI', 'usd_retail_step', 78, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd_base', 'USD TABAN', 'usd_base', 58, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd1', 'USD 1', 'usd1', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd2', 'USD 2', 'usd2', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd3', 'USD 3', 'usd3', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd4', 'USD 4', 'usd4', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd5', 'USD 5', 'usd5', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
  col('usd6', 'USD 6', 'usd6', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),

  col('eur_wholesale_step', 'EUR TOPTAN YUVARLAMA', 'eur_wholesale_step', 76, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur_retail_mode', 'EUR PERAKENDE MODU', 'eur_retail_mode', 76, { classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur_retail_step', 'EUR PERAKENDE DEGERI', 'eur_retail_step', 78, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur_base', 'EUR TABAN', 'eur_base', 58, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur1', 'EUR 1', 'eur1', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur2', 'EUR 2', 'eur2', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur3', 'EUR 3', 'eur3', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur4', 'EUR 4', 'eur4', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur5', 'EUR 5', 'eur5', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
  col('eur6', 'EUR 6', 'eur6', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' })
]

const stickyColumnNames = [
  'copy_select', 'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
  'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
  'anchor_mode', 'calc_enabled', 'publish_postgres', 'publish_nebim'
]
const stickyBoundaryColumnName = 'publish_nebim'
const stickyColumnNameSet = new Set(stickyColumnNames)

const stickyLeftMap = computed(() => {
  const map = {}
  let left = 0
  for (const colName of stickyColumnNames) {
    const column = columns.find(item => item.name === colName)
    if (!column) continue
    map[colName] = left
    left += extractWidth(column.style)
  }
  return map
})

const stickyScrollComp = computed(() => {
  const boundary = columns.find(item => item.name === stickyBoundaryColumnName)
  return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundary?.style)
})

const tableMinWidth = computed(() => columns.reduce((sum, column) => sum + extractWidth(column.style), 0))
const tableStyle = computed(() => ({
  width: `${tableMinWidth.value}px`,
  minWidth: `${tableMinWidth.value}px`,
  tableLayout: 'fixed'
}))

function filterDisplayValue (row, field) {
  if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
  if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
  if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
  return String(row?.[field] ?? '').trim()
}

const multiFilterOptionMap = computed(() => {
  const map = {}
  for (const field of multiFilterFields) {
    const uniq = new Set()
    for (const row of rows.value) {
      const value = filterDisplayValue(row, field)
      if (value) uniq.add(value)
    }
    map[field] = [...uniq]
      .sort((a, b) => a.localeCompare(b, 'tr'))
      .map(value => ({ label: value, value }))
  }
  return map
})

const filteredFilterOptionMap = computed(() => {
  const map = {}
  for (const field of multiFilterFields) {
    const search = String(columnFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
    const options = multiFilterOptionMap.value[field] || []
    map[field] = search
      ? options.filter(option => option.label.toLocaleLowerCase('tr').includes(search))
      : options
  }
  return map
})

const filteredRows = computed(() => {
  return rows.value.filter(row => {
    for (const field of multiFilterFields) {
      const selectedValues = columnFilters.value[field] || []
      if (selectedValues.length > 0 && !selectedValues.includes(filterDisplayValue(row, field))) return false
    }
    for (const field of numericFields) {
      const value = Number(row?.[field] ?? 0)
      const min = parseNullableNumber(numberRangeFilters.value[field]?.min)
      const max = parseNullableNumber(numberRangeFilters.value[field]?.max)
      if (min !== null && value < min) return false
      if (max !== null && value > max) return false
    }
    return true
  })
})

const exportableColumns = computed(() => columns.filter(col => col.name !== 'copy_select' && col.name !== 'select'))
const exportedRows = computed(() => {
  const list = [...filteredRows.value]
  const sortBy = String(tablePagination.value?.sortBy || '').trim()
  const descending = Boolean(tablePagination.value?.descending)
  if (!sortBy) return list

  list.sort((a, b) => {
    const av = exportSortValue(a, sortBy)
    const bv = exportSortValue(b, sortBy)
    if (typeof av === 'number' && typeof bv === 'number') return av - bv
    return String(av ?? '').localeCompare(String(bv ?? ''), 'tr', { numeric: true, sensitivity: 'base' })
  })

  return descending ? list.reverse() : list
})

const copySelectedKeySet = computed(() => new Set(copySelectedKeys.value))
const visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
const selectedVisibleCount = computed(() => visibleRowKeys.value.reduce((count, key) => count + (selectedKeyMap.value?.[key] ? 1 : 0), 0))
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
const selectedDirtyCount = computed(() => rows.value.reduce((count, row) => count + (selectedKeyMap.value?.[row._row_key] && row?._dirty ? 1 : 0), 0))
const selectedCount = computed(() => Object.keys(selectedKeyMap.value || {}).length)
const copySelectedCount = computed(() => copySelectedKeys.value.length)
const canCopySelected = computed(() => copySelectedCount.value >= 2)
const hasUnsavedLocalChanges = computed(() => rows.value.some(row => Boolean(row?._dirty)))
const hasAnyFilter = computed(() => {
  return [...headerFilterFieldSet].some(field => hasFilter(field))
})

function finiteNumber (value, fallback = 0) {
  const parsed = Number(value)
  return Number.isFinite(parsed) ? parsed : fallback
}

function parseNullableNumber (value) {
  if (value === null || value === undefined || String(value).trim() === '') return null
  const parsed = Number(value)
  return Number.isFinite(parsed) ? parsed : null
}

function normalizeWorksheetRow (source) {
  const rule = source?.rule || {}
  const row = {
    pricing_parameter_id: Number(source?.pricing_parameter_id || 0),
    _row_key: String(source?.scope_key || source?.pricing_parameter_id || ''),
    has_rule: Boolean(source?.has_rule),
    id: String(rule?.id || ''),
    anchor_mode: String(rule?.anchor_mode || 'USD'),
    calc_enabled: rule?.calc_enabled !== false,
    publish_postgres: rule?.publish_postgres !== false,
    publish_nebim: rule?.publish_nebim !== false,
    is_active: rule?.is_active !== false,
    askili_yan: String(source?.askili_yan || ''),
    kategori: String(source?.kategori || ''),
    urun_ilk_grubu: String(source?.urun_ilk_grubu || ''),
    urun_ana_grubu: String(source?.urun_ana_grubu || ''),
    urun_alt_grubu: String(source?.urun_alt_grubu || ''),
    icerik: String(source?.icerik || ''),
    marka: String(source?.marka || ''),
    brand_code: String(source?.brand_code || ''),
    brand_group: String(source?.brand_group || ''),
    try_retail_mode: String(rule?.try_retail_mode || 'STEP'),
    usd_retail_mode: String(rule?.usd_retail_mode || 'STEP'),
    eur_retail_mode: String(rule?.eur_retail_mode || 'STEP'),
    _dirty: false
  }
  for (const key of numericFields) {
    row[key] = row.has_rule ? finiteNumber(rule?.[key], 0) : ''
  }
  return row
}

function extractWidth (style) {
  const match = String(style || '').match(/width:(\d+)px/)
  return match ? Number(match[1]) : 0
}

function isStickyCol (colName) {
  return stickyColumnNameSet.has(colName)
}

function isStickyBoundary (colName) {
  return colName === stickyBoundaryColumnName
}

function getHeaderCellStyle (column) {
  if (!isStickyCol(column.name)) return undefined
  return { left: `${stickyLeftMap.value[column.name] || 0}px`, zIndex: 35 }
}

function getBodyCellStyle (column) {
  if (!isStickyCol(column.name)) return undefined
  return { left: `${stickyLeftMap.value[column.name] || 0}px`, zIndex: 12 }
}

function isRowSelected (row) {
  return !!selectedKeyMap.value?.[row._row_key]
}

function clearSelections () {
  selectedKeyMap.value = {}
  copySelectedKeys.value = []
}

function isCopySelected (row) {
  return copySelectedKeySet.value.has(row._row_key)
}

function setRowSelected (row, value) {
  const key = row?._row_key
  if (!key) return
  const next = { ...(selectedKeyMap.value || {}) }
  if (value) {
    next[key] = true
  } else {
    delete next[key]
  }
  selectedKeyMap.value = next
}

function toggleCopySelected (row, value) {
  const key = row?._row_key
  if (!key) return
  const next = [...copySelectedKeys.value]
  const idx = next.indexOf(key)
  if (value) {
    if (idx === -1) next.push(key)
  } else if (idx >= 0) {
    next.splice(idx, 1)
  }
  copySelectedKeys.value = next
}

function copyRoleLabel (row) {
  const key = row?._row_key
  if (!key) return ''
  const idx = copySelectedKeys.value.indexOf(key)
  if (idx === 0) return 'Kaynak'
  if (idx > 0) return 'Hedef'
  return ''
}

function toggleSelectAllVisible (value) {
  const next = { ...(selectedKeyMap.value || {}) }
  for (const key of visibleRowKeys.value) {
    if (value) next[key] = true
    else delete next[key]
  }
  selectedKeyMap.value = next
}

function selectDirtyRow (row) {
  setRowSelected(row, true)
}

function markDirty (row) {
  row._dirty = true
  selectDirtyRow(row)
}

function updateNumber (row, field, value) {
  row[field] = String(value ?? '').trim() === '' ? '' : finiteNumber(value, 0)
  markDirty(row)
}

function updateRetailMode (row, field, value) {
  row[field] = retailModeOptions.includes(String(value || '').trim()) ? String(value).trim() : 'STEP'
  markDirty(row)
}

function exportSortValue (row, field) {
  if (field === 'has_rule') return row?.has_rule ? 1 : 0
  if (field === 'is_active') return row?.is_active ? 1 : 0
  if (field === 'calc_enabled') return row?.calc_enabled ? 1 : 0
  if (field === 'publish_postgres') return row?.publish_postgres ? 1 : 0
  if (field === 'publish_nebim') return row?.publish_nebim ? 1 : 0
  if (numericFields.has(field)) return finiteNumber(row?.[field], 0)
  return String(row?.[field] ?? '')
}

function exportCellValue (row, field) {
  if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
  if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
  if (field === 'calc_enabled') return row?.calc_enabled ? 'Aktif' : 'Pasif'
  if (field === 'publish_postgres') return row?.publish_postgres ? 'Evet' : 'Hayir'
  if (field === 'publish_nebim') return row?.publish_nebim ? 'Evet' : 'Hayir'
  // Excel often coerces numeric-looking codes/names; wrap to keep as text when opened/edited in Excel.
  if (field === 'brand_code' || field === 'marka') {
    const text = String(row?.[field] ?? '').trim()
    if (!text) return ''
    return `="${text.replaceAll('"', '""')}"`
  }
  if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
  if (numericFields.has(field)) {
    const value = row?.[field]
    if (value === '' || value === null || value === undefined) return '0'
    return String(finiteNumber(value, 0))
  }
  return String(row?.[field] ?? '').trim()
}

function csvSafe (value) {
  let text = String(value ?? '').replaceAll('\r', ' ').replaceAll('\n', ' ').trim()
  if (text.includes(';') || text.includes('"')) {
    text = `"${text.replaceAll('"', '""')}"`
  }
  return text
}

function exportCurrentView () {
  const cols = exportableColumns.value
  const list = exportedRows.value
  if (cols.length === 0 || list.length === 0) return

  const lines = [cols.map(col => csvSafe(col.label)).join(';')]
  for (const row of list) {
    lines.push(cols.map(col => csvSafe(exportCellValue(row, col.field))).join(';'))
  }

  const bom = '\uFEFF'
  const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `pricing_rules_${new Date().toISOString().slice(0, 10)}.csv`
  document.body.appendChild(a)
  a.click()
  a.remove()
  URL.revokeObjectURL(url)
}

async function exportAllFiltered () {
  try {
    const params = {
      askili_yan: (columnFilters.value.askili_yan || []).join(','),
      kategori: (columnFilters.value.kategori || []).join(','),
      urun_ilk_grubu: (columnFilters.value.urun_ilk_grubu || []).join(','),
      urun_ana_grubu: (columnFilters.value.urun_ana_grubu || []).join(','),
      urun_alt_grubu: (columnFilters.value.urun_alt_grubu || []).join(','),
      icerik: (columnFilters.value.icerik || []).join(','),
      marka: (columnFilters.value.marka || []).join(','),
      brand_code: (columnFilters.value.brand_code || []).join(','),
      brand_group: (columnFilters.value.brand_group || []).join(','),
      sort_by: tablePagination.value?.sortBy || '',
      desc: tablePagination.value?.descending ? 1 : 0
    }

    for (const field of numericFields) {
      const min = String(numberRangeFilters.value[field]?.min || '').trim()
      const max = String(numberRangeFilters.value[field]?.max || '').trim()
      if (min) params[`${field}_min`] = min
      if (max) params[`${field}_max`] = max
    }

    const blob = await download('/pricing/pricing-rules/export-all', params)
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `pricing_rules_all_${new Date().toISOString().slice(0, 10)}.csv`
    document.body.appendChild(a)
    a.click()
    a.remove()
    URL.revokeObjectURL(url)
  } catch (err) {
    Notify.create({ type: 'negative', message: err?.message || 'Tum filtre export alinamadi' })
  }
}

function openImportDialog () {
  fileInputRef.value?.click?.()
}

function parseCsvRows (text) {
  const clean = String(text || '').replace(/^\uFEFF/, '')
  const rows = []
  let cell = ''
  let row = []
  let inQuotes = false

  for (let i = 0; i < clean.length; i++) {
    const ch = clean[i]
    if (inQuotes) {
      if (ch === '"') {
        if (clean[i + 1] === '"') {
          cell += '"'
          i += 1
        } else {
          inQuotes = false
        }
      } else {
        cell += ch
      }
      continue
    }

    if (ch === '"') {
      inQuotes = true
      continue
    }
    if (ch === ';') {
      row.push(cell)
      cell = ''
      continue
    }
    if (ch === '\n') {
      row.push(cell)
      rows.push(row)
      row = []
      cell = ''
      continue
    }
    if (ch !== '\r') {
      cell += ch
    }
  }

  if (cell !== '' || row.length > 0) {
    row.push(cell)
    rows.push(row)
  }

  return rows
}

function normalizeImportText (value) {
  let text = String(value ?? '').trim()
  if (text.startsWith("'")) text = text.slice(1).trim()
  const formulaMatch = text.match(/^=\s*"([\s\S]*)"$/)
  if (formulaMatch) text = formulaMatch[1]
  return text.toLocaleUpperCase('tr')
}

function buildImportRowKeyFromObject (row) {
  return importKeyFieldLabels
    .map(([field]) => normalizeImportText(row?.[field] ?? ''))
    .join('|')
}

function parseImportedNumber (value) {
  const text = String(value ?? '').trim().replace(/\s/g, '')
  if (!text) return 0

  const lastComma = text.lastIndexOf(',')
  const lastDot = text.lastIndexOf('.')
  let normalized = text

  if (lastComma >= 0 && lastDot >= 0) {
    if (lastComma > lastDot) normalized = text.replace(/\./g, '').replace(',', '.')
    else normalized = text.replace(/,/g, '')
  } else if (lastComma >= 0) {
    normalized = text.replace(/\./g, '').replace(',', '.')
  } else {
    normalized = text.replace(/,/g, '')
  }

  const parsed = Number(normalized)
  return Number.isFinite(parsed) ? parsed : 0
}

function parseImportedBoolean (value) {
  const normalized = normalizeImportText(value)
  if (!normalized) return null
  if (['AKTIF', 'TRUE', '1', 'EVET', 'YES'].includes(normalized)) return true
  if (['PASIF', 'FALSE', '0', 'HAYIR', 'NO'].includes(normalized)) return false
  return null
}

async function onImportFileChange (event) {
  const input = event?.target
  const file = input?.files?.[0]
  if (!file) return

  try {
    const startedAt = Date.now()
    console.info('[pricing-rules][ui] csv-import:start', {
      at: new Date(startedAt).toISOString(),
      name: file?.name || '',
      size: file?.size || 0
    })
    const text = await file.text()
    const matrix = parseCsvRows(text).filter(row => row.some(cell => String(cell || '').trim() !== ''))
    if (matrix.length < 2) {
      Notify.create({ type: 'negative', message: 'CSV bos veya gecersiz' })
      csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV bos veya gecersiz.' }
      return
    }

    const headers = matrix[0].map(cell => String(cell || '').trim())
    const keyHeaderIndexes = {}
    for (const [, label] of importKeyFieldLabels) {
      keyHeaderIndexes[label] = headers.indexOf(label)
    }

    const missingKeyHeaders = importKeyFieldLabels
      .map(([, label]) => label)
      .filter(label => keyHeaderIndexes[label] < 0)
    if (missingKeyHeaders.length > 0) {
      Notify.create({ type: 'negative', message: `Eksik kolon: ${missingKeyHeaders.join(', ')}` })
      return
    }

    const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row]))
    let matched = 0
    let updated = 0
    let skipped = 0
    const updatedRowKeys = []

    for (let i = 1; i < matrix.length; i++) {
      const csvRow = matrix[i]
      const identity = {}
      for (const [field, label] of importKeyFieldLabels) {
        identity[field] = String(csvRow[keyHeaderIndexes[label]] ?? '').trim().replace(/^'/, '').replace(/^=\s*"([\s\S]*)"$/, '$1')
      }

      const target = rowMap.get(buildImportRowKeyFromObject(identity))
      if (!target) {
        skipped++
        continue
      }
      matched++

      let rowChanged = false
      for (const [headerLabel, field] of Object.entries(importFieldMap)) {
        const idx = headers.indexOf(headerLabel)
        if (idx < 0) continue
        const rawValue = csvRow[idx] ?? ''

        if (field === 'is_active') {
          const next = parseImportedBoolean(rawValue)
          if (next !== null && Boolean(target.is_active) !== next) {
            target.is_active = next
            rowChanged = true
          }
          continue
        }
        if (field === 'calc_enabled' || field === 'publish_postgres' || field === 'publish_nebim') {
          const next = parseImportedBoolean(rawValue)
          if (next !== null && Boolean(target[field]) !== next) {
            target[field] = next
            rowChanged = true
          }
          continue
        }
        if (retailModeFields.has(field)) {
          const next = retailModeOptions.includes(normalizeImportText(rawValue)) ? normalizeImportText(rawValue) : 'STEP'
          if (String(target[field] || 'STEP') !== next) {
            target[field] = next
            rowChanged = true
          }
          continue
        }
        const next = parseImportedNumber(rawValue)
        if (Number(target[field] ?? 0) !== next) {
          target[field] = next
          rowChanged = true
        }
      }

      if (rowChanged) {
        markDirty(target)
        updatedRowKeys.push(String(target._row_key || '').trim())
        updated++
      }
    }

    if (matched === 0) {
      Notify.create({ type: 'warning', message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi' })
      csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi.' }
      return
    }

    // Ensure: CSV'den degisen satirlar hem dirty hem de "Kaydet" secimi (checkbox) olarak isaretlensin.
    // Bazı render edge-case'lerinde sadece sayac artip checkbox guncellenmiyor gibi gorunebiliyor;
    // burada selection map'i explicit guncelleyip senkronu garanti ediyoruz.
    if (updatedRowKeys.length > 0) {
      const next = { ...(selectedKeyMap.value || {}) }
      for (const key of updatedRowKeys) {
        if (!key) continue
        next[key] = true
      }
      selectedKeyMap.value = next
    }

    const summary = `CSV yuklendi. Islenen: ${matrix.length - 1}, eslesen: ${matched}, guncellenen: ${updated}, atlanan: ${skipped}`
    if (updated === 0) {
      Notify.create({ type: 'warning', message: summary })
      csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: summary }
    } else {
      Notify.create({ type: 'positive', message: summary })
      csvImportStatus.value = { type: 'positive', at: new Date().toISOString(), message: summary }
    }

    console.info('[pricing-rules][ui] csv-import:done', {
      duration_ms: Date.now() - startedAt,
      processed: matrix.length - 1,
      matched,
      updated,
      skipped
    })
  } catch (err) {
    Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
    csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: err?.message || 'CSV okunamadi' }
  } finally {
    if (input) input.value = ''
  }
}

function copySelectedToSelected () {
  const keys = [...copySelectedKeys.value]
  if (keys.length < 2) return
  const sourceKey = keys[0]
  const source = rows.value.find(row => row._row_key === sourceKey)
  if (!source) return

  for (let i = 1; i < keys.length; i++) {
    const target = rows.value.find(row => row._row_key === keys[i])
    if (!target) continue
    target.is_active = Boolean(source.is_active)
    target.calc_enabled = Boolean(source.calc_enabled)
    target.publish_postgres = Boolean(source.publish_postgres)
    target.publish_nebim = Boolean(source.publish_nebim)
    target.try_retail_mode = String(source.try_retail_mode || 'STEP')
    target.usd_retail_mode = String(source.usd_retail_mode || 'STEP')
    target.eur_retail_mode = String(source.eur_retail_mode || 'STEP')
    for (const field of numericFields) {
      target[field] = source[field]
    }
    markDirty(target)
  }

  copySelectedKeys.value = []
  Notify.create({ type: 'positive', message: 'Kopyalama tamamlandi' })
}

function isHeaderFilterField (field) {
  return headerFilterFieldSet.has(field)
}

function isMultiSelectFilterField (field) {
  return multiSelectFilterFieldSet.has(field)
}

function isNumberRangeFilterField (field) {
  return numberRangeFilterFieldSet.has(field)
}

function hasFilter (field) {
  if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
  if (isNumberRangeFilterField(field)) {
    const filter = numberRangeFilters.value[field]
    return String(filter?.min || '').trim() !== '' || String(filter?.max || '').trim() !== ''
  }
  return false
}

function getFilterBadgeValue (field) {
  if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
  if (isNumberRangeFilterField(field)) {
    const filter = numberRangeFilters.value[field]
    return [filter?.min, filter?.max].filter(value => String(value || '').trim() !== '').length
  }
  return 0
}

function getFilterOptionsForField (field) {
  return filteredFilterOptionMap.value[field] || []
}

function isColumnFilterValueSelected (field, value) {
  return (columnFilters.value[field] || []).includes(value)
}

function toggleColumnFilterValue (field, value) {
  const current = new Set(columnFilters.value[field] || [])
  if (current.has(value)) current.delete(value)
  else current.add(value)
  columnFilters.value = { ...columnFilters.value, [field]: [...current] }
}

function selectAllColumnFilterOptions (field) {
  columnFilters.value = {
    ...columnFilters.value,
    [field]: getFilterOptionsForField(field).map(option => option.value)
  }
}

function clearColumnFilter (field) {
  columnFilters.value = { ...columnFilters.value, [field]: [] }
}

function updateNumberRangeFilter (field, side, value) {
  numberRangeFilters.value = {
    ...numberRangeFilters.value,
    [field]: { ...numberRangeFilters.value[field], [side]: value }
  }
}

function clearRangeFilter (field) {
  numberRangeFilters.value = {
    ...numberRangeFilters.value,
    [field]: { min: '', max: '' }
  }
}

function clearAllFilters () {
  columnFilters.value = Object.fromEntries(multiFilterFields.map(field => [field, []]))
  columnFilterSearch.value = Object.fromEntries(multiFilterFields.map(field => [field, '']))
  numberRangeFilters.value = Object.fromEntries([...numericFields].map(field => [field, { min: '', max: '' }]))
}

async function refreshRows () {
  if (hasUnsavedLocalChanges.value) {
    const confirmed = await new Promise(resolve => {
      Dialog.create({
        title: 'Degisiklikler silinecek',
        message: 'Kaydedilmemis tum degisiklikler silinecek. Devam etmek istiyor musunuz?',
        cancel: true,
        persistent: true
      })
        .onOk(() => resolve(true))
        .onCancel(() => resolve(false))
        .onDismiss(() => resolve(false))
    })
    if (!confirmed) return
  }

  clearAllFilters()
  clearSelections()
  await loadRows()
}

async function loadRows () {
  if (emptyRetryTimer) {
    clearTimeout(emptyRetryTimer)
    emptyRetryTimer = null
  }
  loading.value = true
  let ok = false
  try {
    const res = await api.request({
      method: 'GET',
      url: '/pricing/pricing-rules/parameters',
      timeout: 180000
    })
    rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
    clearSelections()
    if (rows.value.length === 0) {
      emptyRetryTimer = setTimeout(loadRows, 10000)
    }
    ok = true
  } catch (err) {
    Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kural kombinasyonlari alinamadi' })
  } finally {
    loading.value = false
  }
  return ok
}

async function saveSelected () {
  const dirty = rows.value.filter(row => selectedKeyMap.value?.[row._row_key] && row?._dirty)
  if (dirty.length === 0) return
  saving.value = true
  try {
    const startedAt = Date.now()
    console.info('[pricing-rules][ui] saveSelected:start', {
      at: new Date(startedAt).toISOString(),
      dirty_count: dirty.length
    })

    const buildPayload = (list) => {
      return {
        items: list.map(row => {
          const item = {
            id: row.id,
            pricing_parameter_id: row.pricing_parameter_id,
            calc_enabled: Boolean(row.calc_enabled),
            publish_postgres: Boolean(row.publish_postgres),
            publish_nebim: Boolean(row.publish_nebim),
            is_active: Boolean(row.is_active),
            try_retail_mode: String(row.try_retail_mode || 'STEP'),
            usd_retail_mode: String(row.usd_retail_mode || 'STEP'),
            eur_retail_mode: String(row.eur_retail_mode || 'STEP')
          }
          for (const key of numericFields) item[key] = finiteNumber(row[key], 0)
          return item
        })
      }
    }

    const makeTraceId = () => `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
    const isTimeoutLikeError = (e) => {
      const status = e?.response?.status || null
      // With axios timeout disabled for bulk-save, treat only real upstream/proxy timeouts as retry-able.
      return status === 504
    }

    let savedTotal = 0
    const failedKeys = []

    const postBulkSave = async (list) => {
      const traceId = makeTraceId()
      const payload = buildPayload(list)
      await api.request({
        method: 'POST',
        url: '/pricing/pricing-rules/bulk-save',
        data: payload,
        // Disable axios timeout here: backend may legitimately run for several minutes on the first write after a full truncate/import.
        // Any upstream/proxy timeout will surface as 504 anyway.
        timeout: 0,
        headers: { 'X-Trace-ID': traceId }
      })
      return traceId
    }

    // Prefer single request (fast path). Fallback to bisection only on proxy/timeout errors.
    try {
      const traceId = await postBulkSave(dirty)
      savedTotal = dirty.length
      console.info('[pricing-rules][ui] saveSelected:one-shot:done', { trace_id: traceId, total: dirty.length })
    } catch (e) {
      if (!isTimeoutLikeError(e)) throw e

      const initialChunkSize = 50
      const queue = []
      for (let offset = 0; offset < dirty.length; offset += initialChunkSize) {
        queue.push(dirty.slice(offset, offset + initialChunkSize))
      }

      while (queue.length > 0) {
        const batch = queue.shift()
        if (!batch || batch.length === 0) continue

        console.info('[pricing-rules][ui] saveSelected:batch:start', {
          batch_size: batch.length,
          saved_total: savedTotal,
          total: dirty.length
        })

        try {
          const traceId = await postBulkSave(batch)
          savedTotal += batch.length
          Notify.create({ type: 'positive', message: `Kaydedildi: ${savedTotal} / ${dirty.length}` })
          console.info('[pricing-rules][ui] saveSelected:batch:done', {
            trace_id: traceId,
            batch_size: batch.length,
            saved_total: savedTotal,
            total: dirty.length
          })
        } catch (err2) {
          if (isTimeoutLikeError(err2) && batch.length > 1) {
            const mid = Math.ceil(batch.length / 2)
            queue.unshift(batch.slice(mid))
            queue.unshift(batch.slice(0, mid))
            continue
          }
          throw err2
        }
      }
    }

    const reloaded = await loadRows()
    if (!reloaded) {
      Notify.create({
        type: 'warning',
        message: 'Kaydetme tamamlandi, ancak liste yenilenemedi. Sayfayi yenileyip (F5) kontrol edin.'
      })
      return
    }
    Notify.create({ type: 'positive', message: `Kaydedildi: ${dirty.length}` })
    csvImportStatus.value = null
    console.info('[pricing-rules][ui] saveSelected:done', {
      duration_ms: Date.now() - startedAt,
      dirty_count: dirty.length,
      reloaded: true
    })
  } catch (err) {
    Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi' })
    console.error('[pricing-rules][ui] saveSelected:error', {
      status: err?.response?.status || null,
      message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi'
    })
  } finally {
    saving.value = false
  }
}

function resetTransientState () {
  rows.value = []
  clearSelections()
}

onMounted(refreshRows)
onActivated(refreshRows)
onBeforeUnmount(() => {
  if (emptyRetryTimer) clearTimeout(emptyRetryTimer)
  resetTransientState()
})
</script>

<style scoped>
.pricing-rules-page {
  --rules-row-height: 27px;
  --rules-header-height: 58px;
  --rules-table-height: calc(100vh - 210px);

  position: relative;
  min-width: 0;
  height: calc(100vh - 120px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.top-bar,
.action-bar {
  flex: 0 0 auto;
  gap: 8px;
  background: #fff;
  position: relative;
  z-index: 40;
}

.table-wrap {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  border: 1px solid rgba(0, 0, 0, 0.12);
  border-radius: 4px;
  display: flex;
  flex-direction: column;
}

.pane-table {
  height: 100%;
  width: 100%;
}

.rules-table :deep(.q-table__middle) {
  height: var(--rules-table-height);
  min-height: var(--rules-table-height);
  max-height: var(--rules-table-height);
  overflow: auto !important;
  scrollbar-gutter: stable both-edges;
  overscroll-behavior: contain;
}

.rules-table :deep(.q-table) {
  width: max-content;
  min-width: 100%;
  table-layout: fixed;
  font-size: 10px;
  border-collapse: separate;
  border-spacing: 0;
  margin-right: var(--sticky-scroll-comp, 0px);
}

.rules-table :deep(.q-table__container) {
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
  height: 100% !important;
}

.rules-table :deep(th),
.rules-table :deep(td) {
  box-sizing: border-box;
  padding: 0 2px;
  overflow: hidden;
  vertical-align: middle;
}

.rules-table :deep(td),
.rules-table :deep(.q-table tbody tr) {
  height: var(--rules-row-height) !important;
  min-height: var(--rules-row-height) !important;
  max-height: var(--rules-row-height) !important;
  line-height: var(--rules-row-height);
  padding: 0 !important;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
}

.rules-table :deep(th),
.rules-table :deep(.q-table thead tr),
.rules-table :deep(.q-table thead tr.header-row-fixed),
.rules-table :deep(.q-table thead th),
.rules-table :deep(.q-table thead tr.header-row-fixed > th) {
  height: var(--rules-header-height) !important;
  min-height: var(--rules-header-height) !important;
  max-height: var(--rules-header-height) !important;
}

.rules-table :deep(th) {
  padding-top: 0;
  padding-bottom: 0;
  white-space: nowrap;
  word-break: normal;
  text-overflow: ellipsis;
  text-align: center;
  font-size: 9px;
  font-weight: 800;
  line-height: 1.15;
}

.rules-table :deep(.q-table thead th) {
  position: sticky;
  top: 0;
  z-index: 30;
  background: #fff;
  vertical-align: middle !important;
}

.rules-table :deep(.sticky-col) {
  position: sticky !important;
  background-clip: padding-box;
}

.rules-table :deep(thead .sticky-col) {
  z-index: 35 !important;
}

.rules-table :deep(tbody .sticky-col) {
  z-index: 12 !important;
}

.rules-table :deep(.sticky-boundary) {
  border-right: 2px solid rgba(25, 118, 210, 0.18) !important;
  box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
}

.header-with-filter {
  display: grid;
  grid-template-columns: 1fr 20px;
  align-items: center;
  column-gap: 4px;
  height: 100%;
  line-height: 1.25;
  overflow: hidden;
}

.header-with-filter > span {
  min-width: 0;
  width: 100%;
  overflow: hidden;
  text-align: center;
  text-overflow: ellipsis;
  white-space: normal;
  font-weight: 800;
  line-height: 1.15;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.header-filter-btn {
  width: 20px;
  height: 20px;
  min-width: 20px;
  justify-self: end;
}

.header-filter-ghost {
  opacity: 0;
  pointer-events: none;
}

.selection-header-copy {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: 9px;
  font-weight: 800;
  color: #bf5b04;
}

.excel-filter-menu {
  min-width: 230px;
  padding: 8px;
}

.range-filter-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}

.excel-filter-actions {
  gap: 4px;
}

.excel-filter-options {
  max-height: 220px;
  margin-top: 8px;
  overflow: auto;
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 4px;
}

.excel-filter-option {
  min-height: 32px;
}

.excel-filter-empty {
  padding: 10px 8px;
  color: #607d8b;
  font-size: 11px;
}

.rules-table :deep(th.ps-col),
.rules-table :deep(td.ps-col) {
  background: #fff;
  color: var(--q-primary);
  font-weight: 700;
}

.rules-table :deep(th.copy-selection-col),
.rules-table :deep(td.copy-selection-col) {
  background: #fff3e8;
  color: #bf5b04;
  padding-left: 0 !important;
  padding-right: 0 !important;
  text-align: center !important;
}

.rules-table :deep(th.save-selection-col),
.rules-table :deep(td.save-selection-col) {
  background: #eef9ef;
  color: #1b7f3a;
  padding-left: 0 !important;
  padding-right: 0 !important;
  text-align: center !important;
}

.rules-table :deep(th.selection-col),
.rules-table :deep(td.selection-col) {
  background: #fff;
  color: var(--q-primary);
  padding-left: 0 !important;
  padding-right: 0 !important;
  text-align: center !important;
}

.rules-table :deep(.selection-col .q-checkbox__inner) {
  color: var(--q-primary);
  font-size: 14px;
}

.copy-cell-wrap {
  min-height: 100%;
}

.rules-table :deep(.rule-select-checkbox) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
  font-size: 20px;
}

.rules-table :deep(th.usd-col),
.rules-table :deep(td.usd-col) {
  background: #ecf9f0;
  color: #178a3e;
  font-weight: 700;
}

.rules-table :deep(th.eur-col),
.rules-table :deep(td.eur-col) {
  background: #fdeeee;
  color: #c62828;
  font-weight: 700;
}

.rules-table :deep(th.try-col),
.rules-table :deep(td.try-col) {
  background: #edf4ff;
  color: #1e63c6;
  font-weight: 700;
}

.cell-text {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  line-height: 1.1;
  padding: 0 3px;
  font-size: 10px;
}

.native-cell-input {
  width: 100%;
  height: 20px;
  box-sizing: border-box;
  padding: 1px 2px;
  border: 1px solid #cfd8dc;
  border-radius: 4px;
  background: #fff;
  font-size: 10px;
  margin: 0;
}

.native-cell-input:focus {
  outline: none;
  border-color: #1976d2;
}

.action-legend :deep(.q-chip) {
  font-size: 10px;
  font-weight: 700;
}

.hidden-file-input {
  display: none;
}
</style>
