<script setup lang="ts" generic="T extends object">
import { computed, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import { kbxGridTheme } from '../theme/kbxGridTheme'
import type {
  KbxGridCellRef, KbxGridColumn, KbxGridColumnPreference, KbxGridContextRequest,
  KbxGridDensity, KbxGridEditingPolicy, KbxGridPasteResult, KbxGridSummary,
  KbxSelectionState, KbxValidationError,
} from '@kbx/contracts'
import { normalizeKbxGridClipboardData } from '../grid/editing'
import { resolveKbxGridStatus } from '../grid/status'
import KbxButton from './KbxButton.vue'
import KbxDataState from './KbxDataState.vue'
import KbxStatus from './KbxStatus.vue'
import KbxSummaryBar from './KbxSummaryBar.vue'

const props = withDefaults(defineProps<{
  rows: T[]
  columns: KbxGridColumn<T>[]
  rowKey: keyof T & string
  loading?: boolean
  errorText?: string
  emptyText?: string
  selection?: 'none' | 'single' | 'multiple'
  selectionState?: KbxSelectionState<string>
  totalCount?: number
  allowAllFilteredSelection?: boolean
  editable?: boolean
  editingPolicy?: KbxGridEditingPolicy
  clipboard?: boolean
  personalization?: boolean
  savedPreference?: KbxGridColumnPreference[]
  exportable?: boolean
  exportFileName?: string
  density?: KbxGridDensity
  errors?: KbxValidationError[]
  changedCells?: KbxGridCellRef<string>[]
  summary?: KbxGridSummary<T>[]
  activeRowKey?: string | number | null
  ariaLabel?: string
}>(), {
  loading:false,
  errorText:'',
  emptyText:'조회된 데이터가 없습니다.',
  selection:'none',
  totalCount:0,
  allowAllFilteredSelection:false,
  editable:false,
  clipboard:true,
  personalization:false,
  exportable:false,
  exportFileName:'kbx-grid.csv',
  density:'compact',
  errors:()=>[],
  changedCells:()=>[],
  summary:()=>[],
  activeRowKey:null,
  ariaLabel:'업무 데이터',
})

const emit = defineEmits<{
  selectionChanged:[T[]]
  selectionStateChanged:[KbxSelectionState<string>]
  rowDoubleClicked:[T]
  drillDownRequested:[T]
  cellChanged:[{row:T;field:keyof T & string;oldValue:unknown;newValue:unknown}]
  lookupRequested:[{row:T;field:keyof T & string;entity:string}]
  rowAddRequested:[]
  rowDuplicateRequested:[T[]]
  fillDownApplied:[{field:keyof T & string;count:number}]
  pasteProcessed:[KbxGridPasteResult]
  errorFocusChanged:[KbxGridCellRef<string>]
  contextRequested:[KbxGridContextRequest<T>]
  retry:[]
  preferenceChanged:[KbxGridColumnPreference[]]
  preferenceReset:[]
}>()

const gridApi=ref<any>(null)
const selectedRows=ref<T[]>([])
const allFilteredActive=ref(props.selectionState?.mode==='all-filtered')
const excludedIds=ref<string[]>(props.selectionState?.excludedIds??[])
const errorCursor=ref(-1)
let syncingSelection=false

const gridErrors=computed(()=>props.errors.filter(error=>Boolean(error.rowKey&&error.field&&props.columns.some(column=>column.field===error.field))))
const rowSelectionOptions=computed(()=>props.selection==='multiple'
  ? {mode:'multiRow',checkboxes:true,headerCheckbox:true,enableClickSelection:true,selectAll:'currentPage'}
  : props.selection==='single'
    ? {mode:'singleRow',checkboxes:false,enableClickSelection:true}
    : undefined)

watch(()=>props.selectionState, value=>{
  allFilteredActive.value=value?.mode==='all-filtered'
  excludedIds.value=value?.excludedIds??[]
  syncSelectionState()
},{deep:true})

const firstLoading=computed(()=>props.loading&&props.rows.length===0)
const empty=computed(()=>!props.loading&&!props.errorText&&props.rows.length===0)
const blockingError=computed(()=>Boolean(props.errorText)&&props.rows.length===0)
const showToolbar=computed(()=>props.exportable||props.personalization||gridErrors.value.length>0||selectedCount.value>0||Boolean(props.editingPolicy)||canSelectAllFiltered.value)
const selectedCount=computed(()=>allFilteredActive.value?Math.max((props.totalCount||props.rows.length)-excludedIds.value.length,0):selectedRows.value.length)
const canSelectAllFiltered=computed(()=>props.allowAllFilteredSelection&&props.selection==='multiple'&&(props.totalCount||0)>props.rows.length)
const canDuplicate=computed(()=>Boolean(props.editable&&props.editingPolicy?.allowRowDuplicate&&selectedRows.value.length>0&&!allFilteredActive.value))
const canFillDown=computed(()=>Boolean(props.editable&&props.editingPolicy?.fillDown&&selectedRows.value.length>1&&!allFilteredActive.value))

function formatNumber(value:unknown){if(value==null||value==='')return'';return new Intl.NumberFormat('ko-KR').format(Number(value))}
function errorFor(row:T,field:string){const id=String(row[props.rowKey]);return props.errors.find(e=>e.rowKey===id&&e.field===field)}
function changedFor(row:T,field:string){const id=String(row[props.rowKey]);return props.changedCells.some(x=>String(x.rowKey)===id&&x.field===field)}
function columnFor(field:string){return props.columns.find(c=>c.field===field)}
function isEditable(column:KbxGridColumn<T>|undefined,row:T){if(!column||!props.editable)return false;return typeof column.editable==='function'?column.editable(row):(column.editable??false)}
function statusDisplay(column:KbxGridColumn<T>,value:unknown){return resolveKbxGridStatus(column.statusMap,value).label}
function statusRendererParams(column:KbxGridColumn<T>,value:unknown){
  const status=resolveKbxGridStatus(column.statusMap,value)
  return {label:status.label,semantic:status.semantic,unknown:status.unknown,rawValue:status.rawValue}
}

const columnDefs=computed<ColDef<T>[]>(()=>props.columns.map(c=>({
  field:c.field,headerName:c.header,width:c.width,minWidth:c.minWidth,maxWidth:c.maxWidth,pinned:c.pinned,
  editable:params=>Boolean(params.data&&isEditable(c,params.data)),
  sortable:c.sortable??true,filter:c.filterable??true,resizable:true,
  valueFormatter:c.type==='money'||c.type==='quantity'?p=>formatNumber(p.value):c.type==='status'?p=>statusDisplay(c,p.value):undefined,
  filterValueGetter:c.type==='status'?p=>statusDisplay(c,p.data?.[c.field]):undefined,
  cellRenderer:c.type==='status'?KbxStatus:undefined,
  cellRendererParams:c.type==='status'?p=>statusRendererParams(c,p.value):undefined,
  valueParser:c.type==='money'||c.type==='quantity'||c.type==='integer'||c.type==='decimal'||c.type==='percent'?p=>Number(String(p.newValue??'').replace(/,/g,'')):undefined,
  cellClass:params=>{
    const classes:string[]=[]
    if(c.type==='money'||c.type==='quantity'||c.type==='integer'||c.type==='decimal'||c.type==='percent')classes.push('kbx-cell--numeric')
    if(c.type==='date'||c.type==='datetime'||c.type==='status'||c.type==='boolean')classes.push('kbx-cell--center')
    if(c.type==='link'||c.drilldown)classes.push('kbx-cell--link')
    if(params.data&&isEditable(c,params.data))classes.push('kbx-cell--editable')
    if(params.data&&changedFor(params.data,c.field))classes.push('kbx-cell--changed')
    if(params.data&&errorFor(params.data,c.field))classes.push('kbx-cell--invalid')
    return classes
  },
  tooltipValueGetter:params=>params.data?errorFor(params.data,c.field)?.message:undefined,
  suppressKeyboardEvent:params=>Boolean(c.lookup&&params.event.key==='F2'),
})))

const summaryItems=computed(()=>props.summary.map(item=>{
  let value:string|number=item.value??''
  if(item.kind==='count')value=props.totalCount||props.rows.length
  else if(item.kind==='sum'&&item.field)value=props.rows.reduce((sum,row)=>sum+(Number(row[item.field!])||0),0)
  return {key:item.key,label:item.label,value}
}))

function currentPreference():KbxGridColumnPreference[]{
  const state=(gridApi.value?.getColumnState?.()??[]) as any[]
  return state.map((x,index)=>({field:String(x.colId),order:index,width:x.width,pinned:x.pinned??null,hidden:Boolean(x.hide),sort:x.sort??null,sortIndex:x.sortIndex??null}))
}
function savePreference(){if(props.personalization&&gridApi.value)emit('preferenceChanged',currentPreference())}
function restorePreference(){
  if(!props.personalization||!props.savedPreference?.length||!gridApi.value)return
  const ordered=[...props.savedPreference].sort((a,b)=>a.order-b.order)
  gridApi.value.applyColumnState({state:ordered.map(x=>({colId:x.field,width:x.width,pinned:x.pinned,hide:x.hidden,sort:x.sort,sortIndex:x.sortIndex})),applyOrder:true})
}
function resetColumns(){gridApi.value?.resetColumnState();emit('preferenceReset')}
function exportCsv(){gridApi.value?.exportDataAsCsv({fileName:props.exportFileName,processCellCallback:(params:any)=>{const column=columnFor(String(params.column?.getColId?.()??''));return column?.type==='status'?statusDisplay(column,params.value):params.value}})}
function syncActiveRow(){
  if(!gridApi.value||props.activeRowKey==null)return
  const node=gridApi.value.getRowNode?.(String(props.activeRowKey));if(!node)return
  if(props.selection==='single'){gridApi.value.deselectAll?.();node.setSelected?.(true)}
  if(node.rowIndex!=null)gridApi.value.ensureNodeVisible?.(node,'middle')
}
function syncSelectionState(){
  if(!gridApi.value||props.selection==='none')return
  const state=props.selectionState
  if(!state)return
  syncingSelection=true
  try{
    if(state.mode==='all-filtered'){
      const excluded=new Set(state.excludedIds??[])
      const count=gridApi.value.getDisplayedRowCount?.()??0
      for(let index=0;index<count;index++){
        const node=gridApi.value.getDisplayedRowAtIndex?.(index)
        if(!node?.data)continue
        node.setSelected?.(!excluded.has(String(node.data[props.rowKey])))
      }
    }else{
      const selected=new Set(state.selectedIds??[])
      gridApi.value.forEachNode?.((node:any)=>{if(node?.data)node.setSelected?.(selected.has(String(node.data[props.rowKey])))})
    }
    selectedRows.value=(gridApi.value.getSelectedRows?.()??[]) as T[]
  }finally{queueMicrotask(()=>{syncingSelection=false})}
}
function selectRowByKey(rowKey:string|number|null){
  if(!gridApi.value)return
  syncingSelection=true
  try{
    gridApi.value.deselectAll?.()
    if(rowKey!=null){const node=gridApi.value.getRowNode?.(String(rowKey));node?.setSelected?.(true);if(node?.rowIndex!=null)gridApi.value.ensureNodeVisible?.(node,'middle')}
    selectedRows.value=(gridApi.value.getSelectedRows?.()??[]) as T[]
  }finally{queueMicrotask(()=>{syncingSelection=false})}
}
function clearSelection(){selectRowByKey(null)}
function focusCell(rowKey:string|number,field:string){
  if(!gridApi.value)return false
  const node=gridApi.value.getRowNode?.(String(rowKey));if(node?.rowIndex==null||!columnFor(field))return false
  gridApi.value.ensureNodeVisible?.(node,'middle');gridApi.value.setFocusedCell?.(node.rowIndex,field);return true
}
function focusNextEditableCell(rowKey:string|number,field:string){
  if(!gridApi.value)return false
  const node=gridApi.value.getRowNode?.(String(rowKey));if(node?.rowIndex==null)return false
  const visible=(gridApi.value.getAllDisplayedColumns?.()??[]).map((column:any)=>String(column.getColId?.()??''))
  const start=visible.indexOf(field)
  for(let index=Math.max(start+1,0);index<visible.length;index++){const column=columnFor(visible[index]);if(column&&node.data&&isEditable(column,node.data)){gridApi.value.setFocusedCell?.(node.rowIndex,column.field);gridApi.value.startEditingCell?.({rowIndex:node.rowIndex,colKey:column.field});return true}}
  const nextNode=gridApi.value.getDisplayedRowAtIndex?.(node.rowIndex+1)
  if(nextNode?.data){for(const nextField of visible){const column=columnFor(nextField);if(column&&isEditable(column,nextNode.data)){gridApi.value.ensureNodeVisible?.(nextNode,'middle');gridApi.value.setFocusedCell?.(nextNode.rowIndex,column.field);return true}}}
  return false
}
function onGridReady(event:any){gridApi.value=event.api;restorePreference();syncActiveRow();syncSelectionState()}
watch([()=>props.activeRowKey,()=>props.rows],()=>{syncActiveRow();syncSelectionState()},{deep:true,flush:'post'})

function onSelectionChanged(event:any){
  const rows=event.api.getSelectedRows() as T[]
  selectedRows.value=rows
  if(syncingSelection)return
  emit('selectionChanged',rows)
  const ids=rows.map(row=>String(row[props.rowKey]))
  if(allFilteredActive.value){
    const pageIds=props.rows.map(row=>String(row[props.rowKey]))
    const selected=new Set(ids)
    const excluded=new Set(excludedIds.value)
    for(const id of pageIds) selected.has(id)?excluded.delete(id):excluded.add(id)
    excludedIds.value=[...excluded]
    emit('selectionStateChanged',{mode:'all-filtered',selectedIds:[],excludedIds:excludedIds.value})
  }else emit('selectionStateChanged',{mode:'explicit',selectedIds:ids})
}

function toggleAllFiltered(){
  if(allFilteredActive.value){
    allFilteredActive.value=false;excludedIds.value=[];gridApi.value?.deselectAll();emit('selectionStateChanged',{mode:'explicit',selectedIds:[]});return
  }
  allFilteredActive.value=true;excludedIds.value=[];gridApi.value?.selectAll();emit('selectionStateChanged',{mode:'all-filtered',selectedIds:[],excludedIds:[]})
}

function onCellKeyDown(event:any){
  if(event.event?.key!=='F2'||!event.data||!event.colDef?.field)return
  const column=columnFor(event.colDef.field);if(!column?.lookup)return
  event.event.preventDefault();emit('lookupRequested',{row:event.data,field:column.field,entity:column.lookup.entity})
}
function onCellValueChanged(event:any){emit('cellChanged',{row:event.data,field:event.colDef.field,oldValue:event.oldValue,newValue:event.newValue})}
function onCellClicked(event:any){const column=columnFor(event.colDef?.field);if(event.data&&(column?.type==='link'||column?.drilldown))emit('drillDownRequested',event.data)}
function onCellContextMenu(event:any){
  if(!event.data||!event.colDef?.field)return
  event.event?.preventDefault?.()
  emit('contextRequested',{row:event.data,field:event.colDef.field,clientX:event.event?.clientX??0,clientY:event.event?.clientY??0})
}
function duplicateRows(){if(canDuplicate.value)emit('rowDuplicateRequested',[...selectedRows.value])}
function fillDown(){
  if(!canFillDown.value||!gridApi.value)return
  gridApi.value.stopEditing?.()
  const focused=gridApi.value.getFocusedCell?.();if(!focused)return
  const field=String(focused.column?.getColId?.()??'');const column=columnFor(field);if(!column)return
  const sourceNode=gridApi.value.getDisplayedRowAtIndex?.(focused.rowIndex);const source=sourceNode?.data as T|undefined
  if(!source||!isEditable(column,source))return
  const value=(source as any)[field]
  let count=0
  for(const node of gridApi.value.getSelectedNodes?.()??[]){
    if(!node?.data||node===sourceNode||!isEditable(column,node.data))continue
    node.setDataValue?.(field,value);count++
  }
  if(count)emit('fillDownApplied',{field:column.field,count})
}
function focusError(direction:1|-1=1){
  if(!gridErrors.value.length||!gridApi.value)return
  errorCursor.value=(errorCursor.value+direction+gridErrors.value.length)%gridErrors.value.length
  const error=gridErrors.value[errorCursor.value];if(!error?.rowKey||!error.field)return
  const node=gridApi.value.getRowNode?.(String(error.rowKey));if(node?.rowIndex==null)return
  gridApi.value.ensureNodeVisible?.(node,'middle');gridApi.value.setFocusedCell?.(node.rowIndex,error.field)
  emit('errorFocusChanged',{rowKey:String(error.rowKey),field:error.field})
}
defineExpose({exportCsv,focusError,focusCell,focusNextEditableCell,selectRowByKey,clearSelection})

function processClipboardData(params:any){
  if(!props.clipboard||props.editingPolicy?.paste===false)return null
  const displayed=(params.api?.getAllDisplayedColumns?.()??[]).map((c:any)=>columnFor(String(c.getColId?.()))).filter(Boolean) as KbxGridColumn<T>[]
  const focused=params.api?.getFocusedCell?.();const start=displayed.findIndex(c=>c.field===String(focused?.column?.getColId?.()??''))
  const normalized=normalizeKbxGridClipboardData(params.data??[],displayed,Math.max(start,0));emit('pasteProcessed',normalized.result);return normalized.data
}
</script>

<template>
  <section class="kbx-grid" :data-density="density" :aria-busy="loading" role="region" :aria-label="ariaLabel">
    <KbxDataState v-if="firstLoading" state="loading" />
    <KbxDataState v-else-if="blockingError" state="error" :detail="errorText" action-label="다시 조회" @action="emit('retry')" />
    <KbxDataState v-else-if="empty" state="empty" :title="emptyText" action-label="다시 조회" @action="emit('retry')" />

    <template v-else>
      <div v-if="showToolbar" class="kbx-grid__toolbar" role="toolbar" aria-label="그리드 도구">
        <strong v-if="selectedCount" class="kbx-grid__selection">{{selectedCount.toLocaleString('ko-KR')}}건 선택<span v-if="allFilteredActive"> · 검색결과 전체</span></strong>
        <span v-if="gridErrors.length" class="kbx-grid__error-count">{{gridErrors.length.toLocaleString('ko-KR')}}개 셀 오류</span>
        <span v-if="loading" class="kbx-grid__busy" role="status">재조회 중...</span>
        <KbxButton v-if="editable&&editingPolicy?.allowRowAdd" label="행 추가" variant="ghost" @click="emit('rowAddRequested')" />
        <KbxButton v-if="editable&&editingPolicy?.allowRowDuplicate" label="행 복제" variant="ghost" :disabled="!canDuplicate" @click="duplicateRows" />
        <KbxButton v-if="editable&&editingPolicy?.fillDown" label="아래 채우기" variant="ghost" :disabled="!canFillDown" @click="fillDown" />
        <KbxButton v-if="gridErrors.length&&editingPolicy?.errorNavigation" label="첫/다음 오류" variant="ghost" @click="focusError(1)" />
        <KbxButton v-if="canSelectAllFiltered" :label="allFilteredActive?'전체선택 해제':`검색결과 ${(totalCount||0).toLocaleString('ko-KR')}건 전체선택`" variant="ghost" @click="toggleAllFiltered" />
        <span class="kbx-grid__spacer" />
        <KbxButton v-if="personalization" label="열 배치 초기화" variant="ghost" @click="resetColumns" />
        <KbxButton v-if="exportable" label="CSV 내보내기" variant="ghost" @click="exportCsv" />
      </div>
      <KbxDataState v-if="errorText" state="error" compact :detail="errorText" action-label="다시 조회" @action="emit('retry')" />
      <div class="kbx-grid__body">
        <AgGridVue
          class="kbx-grid__ag"
          :theme="kbxGridTheme"
          :row-data="rows"
          :column-defs="columnDefs"
          :get-row-id="(p:any)=>String(p.data[rowKey])"
          :row-selection="rowSelectionOptions"
          :loading="loading"
          :enter-navigates-vertically="editable"
          :enter-navigates-vertically-after-edit="editable"
          :enable-cell-text-selection="clipboard"
          :suppress-clipboard-paste="!clipboard"
          :process-data-from-clipboard="processClipboardData"
          @grid-ready="onGridReady"
          @column-moved="savePreference"
          @column-resized="savePreference"
          @column-pinned="savePreference"
          @sort-changed="savePreference"
          @selection-changed="onSelectionChanged"
          @row-double-clicked="(e:any)=>emit('rowDoubleClicked',e.data)"
          @cell-clicked="onCellClicked"
          @cell-context-menu="onCellContextMenu"
          @cell-value-changed="onCellValueChanged"
          @cell-key-down="onCellKeyDown"
        />
      </div>
      <KbxSummaryBar v-if="summaryItems.length" :items="summaryItems" />
    </template>
  </section>
</template>

<style scoped>
.kbx-grid{min-height:var(--kbx-grid-min-height);height:100%;display:flex;flex-direction:column;border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface)}.kbx-grid__toolbar{min-height:var(--kbx-grid-toolbar-height);display:flex;align-items:center;gap:var(--kbx-space-2);padding:0 var(--kbx-space-2);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted);flex-wrap:wrap}.kbx-grid__selection{color:var(--kbx-color-text);font-weight:600}.kbx-grid__spacer{flex:1}.kbx-grid__error-count{color:var(--kbx-color-danger);font-weight:600}.kbx-grid__busy{color:var(--kbx-color-primary)}.kbx-grid__body{min-height:0;flex:1}.kbx-grid__ag{width:100%;height:100%;min-height:var(--kbx-grid-min-height)}.kbx-grid[data-density="compact"] :deep(.ag-row){font-size:var(--kbx-font-sm)}.kbx-grid[data-density="comfortable"] :deep(.ag-row){font-size:var(--kbx-font-md)}:deep(.kbx-cell--numeric){text-align:right}:deep(.kbx-cell--center){text-align:center}:deep(.kbx-cell--link){text-decoration:underline;text-underline-offset:var(--kbx-space-1);cursor:pointer}:deep(.kbx-cell--editable){background:var(--kbx-color-warning-surface)}:deep(.ag-cell-focus.kbx-cell--editable){box-shadow:inset 0 0 0 calc(var(--kbx-border-width) * 2) var(--kbx-color-focus)}:deep(.kbx-cell--changed){box-shadow:inset 0 0 0 var(--kbx-border-width) var(--kbx-color-changed-border);background:var(--kbx-color-changed-surface)}:deep(.kbx-cell--invalid){box-shadow:inset 0 0 0 var(--kbx-border-width) var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}
</style>
