/**
 * Pinia store for bitable feature — tables, fields, records, views,
 * and formula recalc status polling.
 *
 * ponytail: Formula recalc polling uses a simple setInterval. The polling
 * stops when no records have pending/calculating formula fields.
 * Ceiling: one polling timer per store instance; if multiple BitableGrid
 * components mount simultaneously they share the same store-level timer.
 */

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { notification } from 'ant-design-vue'
import { bitableApi } from '@/api/bitable'
import type {
  IBitableFile,
  IBitableTable,
  IBitableField,
  IBitableRecord,
  IBitableView,
  FieldType,
} from '@/api/bitable'

export const useBitableStore = defineStore('bitable', () => {
  // --- State ---
  const files = ref<IBitableFile[]>([])
  const currentFile = ref<IBitableFile | null>(null)
  const tables = ref<IBitableTable[]>([])
  const currentTable = ref<IBitableTable | null>(null)
  const fields = ref<IBitableField[]>([])
  const records = ref<IBitableRecord[]>([])
  const views = ref<IBitableView[]>([])
  const currentView = ref<IBitableView | null>(null)
  const isLoading = ref(false)
  const error = ref<string | null>(null)
  const nextCursor = ref<string | null>(null)
  const recalcPendingCount = ref(0)

  // U3: RecordDetailDrawer state. `currentRecordId` is the drawer's open
  // signal — non-null means the drawer is open. `currentRecord` is the
  // fully-hydrated record fetched by record_id (so the drawer sees agent
  // columns even if the grid view hides them).
  const currentRecordId = ref<string | null>(null)
  const currentRecord = ref<IBitableRecord | null>(null)
  const recordDetailLoading = ref(false)
  const recordDetailError = ref<string | null>(null)
  const recordDetailNotFound = ref(false)

  // Polling timer for formula recalc status
  let _pollTimer: ReturnType<typeof setInterval> | null = null
  const POLL_INTERVAL = 2000 // 2s per plan

  // --- Getters ---
  const formulaFields = computed(() =>
    fields.value.filter((f) => f.field_type === 'formula'),
  )

  const hasFormulaFields = computed(() => formulaFields.value.length > 0)

  // --- File actions (R1) ---

  /** Load all bitable files owned by the current user */
  async function loadFiles(): Promise<void> {
    isLoading.value = true
    error.value = null
    try {
      const resp = await bitableApi.listFiles()
      files.value = resp.files || []
    } catch (err) {
      error.value = err instanceof Error ? err.message : '加载文件列表失败'
      notification.error({ message: '加载失败', description: error.value })
    } finally {
      isLoading.value = false
    }
  }

  /**
   * Create a new bitable file.
   *
   * The ``icon`` argument is a stable Bitable icon key (e.g. ``"table"``,
   * ``"dashboard"``) resolved client-side through ``bitableIcons.ts`` —
   * see that module for the full key-to-component map and backward-
   * compatibility fallback (legacy emoji strings map to the default
   * ``table`` icon).
   */
  async function createFile(
    name: string,
    icon: string = 'table',
    description = '',
  ): Promise<IBitableFile | null> {
    try {
      const resp = await bitableApi.createFile({ name, icon, description })
      files.value.unshift(resp.file)
      return resp.file
    } catch (err) {
      notification.error({
        message: '创建文件失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Update a file's metadata */
  async function updateFile(
    fileId: string,
    data: { name?: string; icon?: string; description?: string },
  ): Promise<IBitableFile | null> {
    try {
      const resp = await bitableApi.updateFile(fileId, data)
      const idx = files.value.findIndex((f) => f.id === fileId)
      if (idx >= 0) files.value[idx] = resp.file
      if (currentFile.value?.id === fileId) currentFile.value = resp.file
      return resp.file
    } catch (err) {
      notification.error({
        message: '更新文件失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Delete a file (cascades to all its tables) */
  async function deleteFile(fileId: string): Promise<boolean> {
    try {
      await bitableApi.deleteFile(fileId)
      files.value = files.value.filter((f) => f.id !== fileId)
      if (currentFile.value?.id === fileId) {
        currentFile.value = null
        tables.value = []
        currentTable.value = null
      }
      return true
    } catch (err) {
      notification.error({
        message: '删除文件失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return false
    }
  }

  /** Select a file and load its tables. Falls back to direct fetch on deep-link nav. */
  async function selectFile(fileId: string): Promise<void> {
    let file = files.value.find((f) => f.id === fileId) ?? null
    if (!file) {
      try {
        const resp = await bitableApi.getFile(fileId)
        file = resp.file
      } catch {
        file = null
      }
    }
    currentFile.value = file
    if (!file) return
    await loadTablesByFile(fileId)
  }

  // --- Table actions ---

  /** Load tables belonging to a specific file (R3) */
  async function loadTablesByFile(fileId: string): Promise<void> {
    isLoading.value = true
    error.value = null
    try {
      const resp = await bitableApi.listTablesInFile(fileId)
      tables.value = resp.tables || []
    } catch (err) {
      error.value = err instanceof Error ? err.message : '加载表格列表失败'
      notification.error({ message: '加载失败', description: error.value })
    } finally {
      isLoading.value = false
    }
  }

  /** Create a new table under the current file (auto-creates 5 default fields) */
  async function createTableInFile(
    name: string,
    description?: string,
  ): Promise<IBitableTable | null> {
    if (!currentFile.value) return null
    try {
      const resp = await bitableApi.createTableInFile(currentFile.value.id, {
        name,
        description,
      })
      tables.value.push(resp.table)
      return resp.table
    } catch (err) {
      notification.error({
        message: '创建表格失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Load all bitable tables (deprecated — prefer loadTablesByFile) */
  async function loadTables(): Promise<void> {
    isLoading.value = true
    error.value = null
    try {
      const resp = await bitableApi.listTables()
      tables.value = resp.tables || []
    } catch (err) {
      error.value = err instanceof Error ? err.message : '加载表格列表失败'
      notification.error({ message: '加载失败', description: error.value })
    } finally {
      isLoading.value = false
    }
  }

  /** Select a table and load its fields + records */
  async function selectTable(tableId: string): Promise<void> {
    stopPolling()
    records.value = []
    nextCursor.value = null
    recalcPendingCount.value = 0
    views.value = []
    currentView.value = null

    const table = tables.value.find((t) => t.id === tableId)
    currentTable.value = table || null

    if (!table) return

    try {
      const [fieldsResp, recordsResp, viewsResp] = await Promise.all([
        bitableApi.listFields(tableId),
        bitableApi.listRecords(tableId, { limit: 100 }),
        bitableApi.listViews(tableId),
      ])
      fields.value = fieldsResp.fields || []
      records.value = recordsResp.records || []
      nextCursor.value = recordsResp.next_cursor
      views.value = viewsResp.views || []

      // Start polling if there are formula fields
      if (hasFormulaFields.value) {
        startPolling(tableId)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : '加载表格数据失败'
      notification.error({ message: '加载失败', description: error.value })
    }
  }

  /** Load more records (cursor pagination) */
  async function loadMoreRecords(): Promise<void> {
    if (!currentTable.value || !nextCursor.value) return
    try {
      const resp = await bitableApi.listRecords(currentTable.value.id, {
        cursor: nextCursor.value,
        limit: 100,
      })
      records.value.push(...(resp.records || []))
      nextCursor.value = resp.next_cursor
    } catch (err) {
      notification.error({
        message: '加载更多失败',
        description: err instanceof Error ? err.message : String(err),
      })
    }
  }

  /** Update a single cell value */
  async function updateCell(
    recordId: string,
    fieldId: string,
    value: unknown,
  ): Promise<void> {
    try {
      const resp = await bitableApi.updateRecord(recordId, { [fieldId]: value })
      // Update local state
      const idx = records.value.findIndex((r) => r.id === recordId)
      if (idx >= 0) {
        records.value[idx] = resp.record
      }
    } catch (err) {
      notification.error({
        message: '更新失败',
        description: err instanceof Error ? err.message : String(err),
      })
    }
  }

  /** Add a new field */
  async function addField(
    name: string,
    fieldType: FieldType,
    config?: Record<string, unknown>,
  ): Promise<IBitableField | null> {
    if (!currentTable.value) return null
    try {
      const resp = await bitableApi.createField(currentTable.value.id, {
        name,
        field_type: fieldType,
        config,
      })
      fields.value.push(resp.field)
      return resp.field
    } catch (err) {
      notification.error({
        message: '创建字段失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Create a new table */
  async function createTable(
    name: string,
    description?: string,
  ): Promise<IBitableTable | null> {
    try {
      const resp = await bitableApi.createTable({ name, description })
      tables.value.push(resp.table)
      return resp.table
    } catch (err) {
      notification.error({
        message: '创建表格失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Update an existing field (U2: field_type added for inline type change) */
  async function updateField(
    fieldId: string,
    data: { name?: string; field_type?: FieldType; config?: Record<string, unknown> },
  ): Promise<IBitableField | null> {
    try {
      const resp = await bitableApi.updateField(fieldId, data)
      const idx = fields.value.findIndex((f) => f.id === fieldId)
      if (idx >= 0) {
        fields.value[idx] = resp.field
      }
      return resp.field
    } catch (err) {
      notification.error({
        message: '更新字段失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Delete a field; returns dependencies on 409 */
  async function deleteField(
    fieldId: string,
    force = false,
  ): Promise<{ success: boolean; dependencies?: Record<string, unknown> }> {
    try {
      await bitableApi.deleteField(fieldId, force)
      fields.value = fields.value.filter((f) => f.id !== fieldId)
      return { success: true }
    } catch (err) {
      const apiErr = err as { status?: number; detail?: unknown }
      // 409 = has dependencies, return them for UI confirmation
      if (apiErr.status === 409 && apiErr.detail) {
        const detail = apiErr.detail as Record<string, unknown>
        return { success: false, dependencies: detail.dependencies as Record<string, unknown> }
      }
      notification.error({
        message: '删除字段失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return { success: false }
    }
  }

  /** Hide a field in the current view (R5 column header menu) */
  async function hideField(fieldId: string): Promise<void> {
    if (!currentView.value) return
    const config = { ...currentView.value.config }
    const hiddenFields = (config.hidden_fields as string[]) ?? []
    if (!hiddenFields.includes(fieldId)) {
      hiddenFields.push(fieldId)
    }
    config.hidden_fields = hiddenFields
    await updateView(currentView.value.id, { config })
  }

  /** Refresh records (e.g. after Agent writes data via BitableTool) */
  async function refreshRecords(): Promise<void> {
    if (!currentTable.value) return
    try {
      const filters = currentView.value?.config?.filters as unknown[] | undefined
      const resp = await bitableApi.listRecords(currentTable.value.id, {
        limit: 100,
        filters: filters ? JSON.stringify(filters) : undefined,
      })
      records.value = resp.records || []
      nextCursor.value = resp.next_cursor
    } catch (err) {
      // Silent fail on refresh — user didn't explicitly request it
      console.warn('Failed to refresh records:', err)
    }
  }

  // --- U3: Record detail drawer ---

  /**
   * Open the record detail drawer for a given record id.
   *
   * Sets the drawer open signal and triggers an async fetch of the full
   * record (by record_id) so the drawer sees all fields including agent-
   * owned columns that may be hidden in the grid view.
   *
   * ponytail: fetch is fire-and-forget — `currentRecord` is null until the
   * request resolves, which the drawer renders as a skeleton (LoadingState).
   * Ceiling: no debounce; rapid row-clicks fire one fetch per click. The
   * drawer's watch on `currentRecordId` short-circuits stale fetches by
   * checking the id still matches when the response arrives.
   */
  function openRecordDetail(recordId: string): void {
    if (currentRecordId.value === recordId) return
    currentRecordId.value = recordId
    currentRecord.value = null
    recordDetailLoading.value = true
    recordDetailError.value = null
    recordDetailNotFound.value = false
    void fetchRecordDetail(recordId)
  }

  /** Close the drawer and clear all drawer state. */
  function closeRecordDetail(): void {
    currentRecordId.value = null
    currentRecord.value = null
    recordDetailLoading.value = false
    recordDetailError.value = null
    recordDetailNotFound.value = false
  }

  /**
   * Fetch a single record by id.
   *
   * The bitable list endpoint doesn't expose a single-record GET, so this
   * reuses `listRecords` and searches the result by id. 404 / empty result →
   * notFound state; network error → error state (drawer shows ErrorState +
   * retry).
   *
   * ponytail: fast path — the record is in the currently-loaded page
   * (`records.value`), no network round-trip. Slow path — re-query the first
   * 100 records and search. Ceiling: a record on a later page (cursor > 100)
   * would 404-false. Acceptable for P0 because the drawer is opened from a
   * visible grid row, so the record is always in the loaded page. Upgrade
   * path: add GET /records/{id} when cross-page detail access is needed.
   */
  async function fetchRecordDetail(recordId: string): Promise<void> {
    if (!currentTable.value) {
      recordDetailLoading.value = false
      recordDetailError.value = '未选择数据表'
      return
    }
    recordDetailLoading.value = true
    recordDetailError.value = null
    recordDetailNotFound.value = false
    try {
      let found: IBitableRecord | null = null
      // Fast path: record is in the currently-loaded page.
      const local = records.value.find((r) => r.id === recordId)
      if (local) {
        found = local
      } else if (currentTable.value) {
        // Slow path: re-query the first 100 records and search by id.
        // (See function docstring — ceiling noted.)
        const resp = await bitableApi.listRecords(currentTable.value.id, { limit: 100 })
        found = (resp.records || []).find((r) => r.id === recordId) ?? null
      }

      // Stale-response guard: if the user clicked another row while this
      // fetch was in flight, drop the result.
      if (currentRecordId.value !== recordId) return

      if (!found) {
        recordDetailNotFound.value = true
      } else {
        currentRecord.value = found
      }
    } catch (err) {
      if (currentRecordId.value !== recordId) return
      recordDetailError.value = err instanceof Error ? err.message : '加载记录详情失败'
    } finally {
      if (currentRecordId.value === recordId) {
        recordDetailLoading.value = false
      }
    }
  }

  /**
   * Save user-edited field values from the drawer.
   *
   * Merges the edited user-owned fields with the EXISTING agent-owned field
   * values from `currentRecord` before PATCHing. This preserves agent
   * columns: the backend's update_record_values does a full-replace of the
   * values dict, so we send the agent columns back as-is rather than
   * dropping them. (Spec: "upsert 保留 agent 列" — implemented client-side
   * to avoid adding a new backend endpoint in U3.)
   *
   * Returns true on success. On success, both `currentRecord` and the
   * matching entry in `records` are updated with the server response.
   */
  async function updateRecordFields(
    recordId: string,
    editedFields: Record<string, unknown>,
  ): Promise<boolean> {
    const base = currentRecord.value?.values ?? {}
    const merged: Record<string, unknown> = { ...base, ...editedFields }
    try {
      const resp = await bitableApi.updateRecord(recordId, merged)
      currentRecord.value = resp.record
      const idx = records.value.findIndex((r) => r.id === recordId)
      if (idx >= 0) records.value[idx] = resp.record
      return true
    } catch (err) {
      notification.error({
        message: '保存失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return false
    }
  }

  // --- View management (U5c) ---

  /** Create a new view for the current table */
  async function createView(
    name: string,
    viewType: IBitableView['view_type'] = 'grid',
    config?: Record<string, unknown>,
  ): Promise<IBitableView | null> {
    if (!currentTable.value) return null
    try {
      const resp = await bitableApi.createView(currentTable.value.id, {
        name,
        view_type: viewType,
        config,
      })
      views.value.push(resp.view)
      currentView.value = resp.view
      return resp.view
    } catch (err) {
      notification.error({
        message: '创建视图失败',
        description: err instanceof Error ? err.message : String(err),
      })
      return null
    }
  }

  /** Update a view's config (filters/sorts/hidden fields) */
  async function updateView(
    viewId: string,
    data: { name?: string; config?: Record<string, unknown> },
  ): Promise<void> {
    try {
      const resp = await bitableApi.updateView(viewId, data)
      const idx = views.value.findIndex((v) => v.id === viewId)
      if (idx >= 0) {
        views.value[idx] = resp.view
      }
      if (currentView.value?.id === viewId) {
        currentView.value = resp.view
        // Re-query records with updated view config
        await refreshRecords()
      }
    } catch (err) {
      notification.error({
        message: '更新视图失败',
        description: err instanceof Error ? err.message : String(err),
      })
    }
  }

  /**
   * U5: merge group_by + conditional_formatting into the view's existing
   * config and PATCH it. Preserves other config keys (filters / sort /
   * hidden_fields) — only the U5 sub-keys are replaced.
   *
   * The backend route layer validates the U5 sub-keys via Pydantic and
   * returns 422 on invalid input (caught here as a notification).
   */
  async function updateViewConfig(
    viewId: string,
    u5Config: { group_by?: unknown[]; conditional_formatting?: unknown[] },
  ): Promise<void> {
    const existing = views.value.find((v) => v.id === viewId)
    // Merge: start from existing config, overwrite only the U5 keys present.
    const mergedConfig: Record<string, unknown> = {
      ...(existing?.config ?? {}),
    }
    if ('group_by' in u5Config) {
      mergedConfig.group_by = u5Config.group_by
    }
    if ('conditional_formatting' in u5Config) {
      mergedConfig.conditional_formatting = u5Config.conditional_formatting
    }
    await updateView(viewId, { config: mergedConfig })
  }

  /** Switch to a view — applies its config to the records query */
  async function switchView(viewId: string): Promise<void> {
    const view = views.value.find((v) => v.id === viewId)
    if (!view) return
    currentView.value = view
    await refreshRecords()
  }

  /**
   * Delete a view (U6: R15a). Removes it from local state on success.
   * If the deleted view was active, switches to the first remaining view.
   * Backend rejects deleting the last view of a table with 409 Conflict.
   * Returns true on success, false on error (notification shown).
   */
  async function deleteView(viewId: string): Promise<boolean> {
    try {
      await bitableApi.deleteView(viewId)
      const wasActive = currentView.value?.id === viewId
      views.value = views.value.filter((v) => v.id !== viewId)
      if (wasActive) {
        // Switch to the first remaining view, if any.
        currentView.value = views.value[0] ?? null
        await refreshRecords()
      }
      return true
    } catch (err) {
      const apiErr = err as { status?: number }
      // 409 = last view of the table — backend forbids deletion.
      if (apiErr.status === 409) {
        notification.warning({
          message: '无法删除',
          description: '至少保留一个视图，不能删除最后一个视图。',
        })
      } else {
        notification.error({
          message: '删除视图失败',
          description: err instanceof Error ? err.message : String(err),
        })
      }
      return false
    }
  }

  // --- Formula recalc polling (R7) ---

  /** Start polling for formula recalc status */
  function startPolling(tableId: string): void {
    stopPolling()
    _pollTimer = setInterval(async () => {
      await pollRecalcStatus(tableId)
    }, POLL_INTERVAL)
  }

  /** Stop the polling timer */
  function stopPolling(): void {
    if (_pollTimer !== null) {
      clearInterval(_pollTimer)
      _pollTimer = null
    }
  }

  /** Poll recalc status: reload records if any formula fields are still calculating */
  async function pollRecalcStatus(tableId: string): Promise<void> {
    try {
      // Reuse current view filters so polling doesn't overwrite a filtered set
      const filters = currentView.value?.config?.filters
      const resp = await bitableApi.listRecords(tableId, {
        limit: 100,
        filters: filters ? JSON.stringify(filters) : undefined,
      })
      const newRecords = resp.records || []

      // Single traversal: collect pending records (formula field values still null)
      const pending = newRecords.filter((rec) =>
        formulaFields.value.some((f) => rec.values[f.id] == null),
      )
      const stillCalculating = pending.length > 0
      const wasCalculating = recalcPendingCount.value > 0

      // Update state when records changed, still calculating, or just finished
      // (wasCalculating ensures the final computed values are applied).
      const oldIds = records.value.map((r) => r.id).join(',')
      const newIds = newRecords.map((r) => r.id).join(',')
      if (oldIds !== newIds || stillCalculating || wasCalculating) {
        records.value = newRecords
        nextCursor.value = resp.next_cursor
      }

      if (stillCalculating) {
        recalcPendingCount.value = pending.length
      } else {
        recalcPendingCount.value = 0
        stopPolling()
      }
    } catch (err) {
      // Silent fail on poll — don't spam notifications
      console.warn('Recalc poll failed:', err)
    }
  }

  return {
    // State
    files,
    currentFile,
    tables,
    currentTable,
    fields,
    records,
    views,
    currentView,
    isLoading,
    error,
    nextCursor,
    recalcPendingCount,
    // U3: record detail drawer state
    currentRecordId,
    currentRecord,
    recordDetailLoading,
    recordDetailError,
    recordDetailNotFound,
    // Getters
    formulaFields,
    hasFormulaFields,
    // File actions (R1)
    loadFiles,
    createFile,
    updateFile,
    deleteFile,
    selectFile,
    loadTablesByFile,
    createTableInFile,
    // Table actions
    loadTables,
    selectTable,
    loadMoreRecords,
    updateCell,
    addField,
    createTable,
    updateField,
    deleteField,
    hideField,
    refreshRecords,
    // U3: record detail drawer actions
    openRecordDetail,
    closeRecordDetail,
    fetchRecordDetail,
    updateRecordFields,
    createView,
    updateView,
    updateViewConfig,
    switchView,
    deleteView,
    stopPolling,
  }
})
