/**
 * Dictionary Catalog — список со связями (View D из handoff).
 *
 * Bundle-grouped cards с scope-color strip, search/scope/bundle filters
 * и "только со связями" toggle. См. design_handoff_dictionary_catalog/
 * (`design/proto/view-list.jsx` + `screenshots/D-list-view.png`).
 *
 * URL state preserved для shareable links: `?q=&scope=PUBLIC,INTERNAL&bundle=cuod&deps=1`.
 *
 * FK chips: outgoing fk[] парсится из schemaJson properties (x-references), но для
 * каталога fetch'ить полный detail per dict expensive (N+1). На каталоге показываем
 * лишь refBy via `useDictionaryDependents` per card (TanStack Query кеширует
 * параллельные запросы). Outgoing fk будет добавлен через batch backend endpoint
 * (followup), сейчас рендерится только если backend начнёт возвращать в DictionaryDefinition.
 */
import { useDeferredValue, useMemo, useState } from 'react'
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@/ui'
import { PlusIcon } from '@phosphor-icons/react'
import { useQueries } from '@tanstack/react-query'
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
import type { DataScope, DictionaryDefinition, SchemaDependent } from '@/api/client'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
import { useCanMutate } from '@/auth/useCanMutate'
import { SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'

// ===== Search params =====

const SCOPE_VALUES = new Set<DataScope>(['PUBLIC', 'INTERNAL', 'RESTRICTED'])

export type CatalogSearch = {
  q?: string
  scope?: string // CSV "PUBLIC,INTERNAL"
  bundle?: string
  deps?: '1'
}

const validateSearch = (raw: Record<string, unknown>): CatalogSearch => {
  const out: CatalogSearch = {}
  if (typeof raw.q === 'string' && raw.q.length > 0) out.q = raw.q
  if (typeof raw.scope === 'string' && raw.scope.length > 0) out.scope = raw.scope
  if (typeof raw.bundle === 'string' && raw.bundle.length > 0) out.bundle = raw.bundle
  if (raw.deps === '1' || raw.deps === 1) out.deps = '1'
  return out
}

// ===== Pure helpers (exported для unit tests) =====

export const parseScopeFilter = (csv: string | undefined): Set<DataScope> => {
  if (!csv) return new Set()
  const out = new Set<DataScope>()
  for (const raw of csv.split(',')) {
    const candidate = raw.trim().toUpperCase()
    if (SCOPE_VALUES.has(candidate as DataScope)) out.add(candidate as DataScope)
  }
  return out
}

export const matchesQuery = (d: DictionaryDefinition, q: string): boolean => {
  if (!q) return true
  const haystack = [d.name, d.displayName ?? '', d.description ?? '']
    .join(' ')
    .toLowerCase()
  return haystack.includes(q)
}

export const groupByBundle = (
  list: DictionaryDefinition[],
): Map<string, DictionaryDefinition[]> => {
  const out = new Map<string, DictionaryDefinition[]>()
  for (const d of list) {
    const key = d.bundle || 'default'
    if (!out.has(key)) out.set(key, [])
    out.get(key)!.push(d)
  }
  // Sort dicts within each bundle by displayName/name
  for (const [, items] of out) {
    items.sort((a, b) =>
      (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name),
    )
  }
  return out
}

// Scope strip color — vertical 3px на левом крае карточки.
// Использует design handoff tokens — auto-switch в dark mode.
const SCOPE_STRIP: Record<DataScope, string> = {
  PUBLIC: 'bg-accent',
  INTERNAL: 'bg-warn',
  RESTRICTED: 'bg-pink',
}

// ===== Route =====

export const Route = createFileRoute('/dictionaries/')({
  validateSearch,
  component: DictionariesPage,
})

function DictionariesPage() {
  const { t } = useTranslation()
  const navigate = useNavigate({ from: '/dictionaries/' })
  const search = Route.useSearch()
  const { data, isLoading, error } = useDictionaries()
  const [createOpen, setCreateOpen] = useState(false)
  const canMutate = useCanMutate()

  const q = (search.q ?? '').trim().toLowerCase()
  const deferredQuery = useDeferredValue(q)
  const scopeFilter = useMemo(() => parseScopeFilter(search.scope), [search.scope])
  const bundleFilter = search.bundle
  const withDepsOnly = search.deps === '1'

  const setSearch = (next: Partial<CatalogSearch>) => {
    navigate({
      search: (prev) => {
        const merged: CatalogSearch = { ...prev, ...next }
        if (!merged.q) delete merged.q
        if (!merged.scope) delete merged.scope
        if (!merged.bundle) delete merged.bundle
        if (!merged.deps) delete merged.deps
        return merged
      },
    })
  }

  const toggleScope = (scope: DataScope) => {
    const next = new Set(scopeFilter)
    if (next.has(scope)) next.delete(scope)
    else next.add(scope)
    setSearch({ scope: next.size === 0 ? undefined : Array.from(next).join(',') })
  }

  const setBundle = (bundle: string | undefined) => setSearch({ bundle })

  // Батчевая загрузка refBy для всех словарей — для активации withDeps filter.
  // useQueries dedup'ит c per-card useDictionaryDependents (тот же queryKey),
  // так что N=37 запросов выполнятся один раз и cached. Запускается ТОЛЬКО
  // когда deps filter активен — для anonymous browsing N+1 burst не нужен.
  const dependentsResults = useQueries({
    queries: withDepsOnly && data
      ? data.map((d) => ({ ...dictionaryDependentsQuery(d.name) }))
      : [],
  })
  const dependentsMap = useMemo(() => {
    const m = new Map<string, number>()
    if (!withDepsOnly || !data) return m
    data.forEach((d, i) => {
      const r = dependentsResults[i]
      // Пока loading или error — считаем 0 (не показываем). После загрузки —
      // refBy.length. Уникализация не нужна для бинарного hasDeps теста.
      m.set(d.name, r?.data?.length ?? 0)
    })
    return m
  }, [data, dependentsResults, withDepsOnly])

  const filtered = useMemo(() => {
    if (!data) return []
    return data.filter((d) => {
      if (scopeFilter.size > 0 && !scopeFilter.has(d.scope)) return false
      if (bundleFilter && d.bundle !== bundleFilter) return false
      if (!matchesQuery(d, deferredQuery)) return false
      // «Со связями» — справочник используется в других справочниках (refBy > 0).
      // Outgoing FK потребовал бы загрузки полной schema per dict — not worth
      // the bandwidth для catalog filter. refBy достаточно для semantic.
      if (withDepsOnly && (dependentsMap.get(d.name) ?? 0) === 0) return false
      return true
    })
  }, [data, deferredQuery, scopeFilter, bundleFilter, withDepsOnly, dependentsMap])

  const bundleCounts = useMemo(() => {
    const out = new Map<string, number>()
    if (data) for (const d of data) out.set(d.bundle, (out.get(d.bundle) ?? 0) + 1)
    return out
  }, [data])

  const scopeCounts = useMemo(() => {
    const out: Record<DataScope, number> = { PUBLIC: 0, INTERNAL: 0, RESTRICTED: 0 }
    if (data) for (const d of data) out[d.scope]++
    return out
  }, [data])

  const grouped = useMemo(() => groupByBundle(filtered), [filtered])
  const filtersActive = Boolean(q) || scopeFilter.size > 0 || bundleFilter || withDepsOnly
  const resetFilters = () => navigate({ search: {} })

  // Guest mode (anonymous) — кнопка «Создать справочник» скрыта. Backend всё
  // равно вернёт 401 на POST, но UI hide избегает confusing UX.
  const createButton = canMutate ? (
    <Button
      type="button"
      variant="primary"
      leftIcon={<PlusIcon weight="bold" size={16} />}
      onClick={() => setCreateOpen(true)}
      className="whitespace-nowrap"
    >
      {t('schema.action.create')}
    </Button>
  ) : null

  if (isLoading) return <LoadingBlock size="md" label={t('loading')} />
  if (error) {
    return (
      <Alert variant="error" title={t('error.failed')}>
        {String(error)}
      </Alert>
    )
  }
  if (!data || data.length === 0) {
    return (
      <div className="space-y-6">
        <PageHeader
          title={t('nav.dictionaries')}
          description={t('dict.list.subtitle')}
          actions={createButton}
        />
        <EmptyState title={t('dict.empty')} />
        <DictionaryEditorDialog
          open={createOpen}
          mode={{ kind: 'create' }}
          onClose={() => setCreateOpen(false)}
          onSuccess={(name) => {
            setCreateOpen(false)
            navigate({ to: '/dictionaries/$name', params: { name } })
          }}
        />
      </div>
    )
  }

  return (
    <section
      aria-label={t('dict.list.heading')}
      className="space-y-6"
    >
      <PageHeader
        title={t('nav.dictionaries')}
        description={t('dict.list.subtitle')}
        actions={createButton}
      />

      {/* Compact toolbar: всё в одной строке (wraps на narrow viewport).
          Tradeoff: scope/bundle chips меньше padding'а + краткие labels.
          UPPERCASE labels оставлены для consistency с brand voice (Tektur-like). */}
      <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
        <div className="flex-1 min-w-[240px] max-w-sm">
          <SearchInput
            value={search.q ?? ''}
            onChange={(e) => setSearch({ q: e.target.value })}
            placeholder={t('dict.list.search.placeholder')}
            aria-label={t('dict.list.search.placeholder')}
          />
        </div>

        <span className="text-mono text-mute whitespace-nowrap tabular-nums">
          {filtered.length}/{data.length}
        </span>

        {/* Scope chips: компактные, dot + count, full label по hover'у через title */}
        <div
          role="group"
          aria-label={t('dict.list.filter.scope')}
          className="flex items-center gap-1"
        >
          {SCOPE_ORDER.map((scope) => {
            const selected = scopeFilter.has(scope)
            const count = scopeCounts[scope]
            return (
              <button
                key={scope}
                type="button"
                onClick={() => toggleScope(scope)}
                aria-pressed={selected}
                title={t(`dict.list.section.${scope}`)}
 className={`text-cap px-2 py-1 rounded-full flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
                  selected
                    ? 'border-accent bg-accent-bg text-accent'
                    : 'border-line hover:border-accent/60 text-ink'
                }`}
              >
                <span
                  className={`inline-block size-1.5 rounded-full ${SCOPE_DOT[scope]}`}
                  aria-hidden="true"
                />
                {t(`dict.list.section.${scope}.short`)}
                <span className="text-mute ml-0.5">{count}</span>
              </button>
            )
          })}
        </div>

        {/* Vertical divider */}
        <span className="h-5 w-px bg-line" aria-hidden="true" />

        {/* Bundle filter — chips inline */}
        <div
          role="group"
          aria-label="Bundle"
          className="flex items-center gap-1"
        >
          <button
            type="button"
            onClick={() => setBundle(undefined)}
            aria-pressed={!bundleFilter}
 className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
              !bundleFilter
                ? 'border-accent bg-accent-bg text-accent'
                : 'border-line hover:border-accent/60 text-ink'
            }`}
          >
            {t('dict.list.bundle.all')}
          </button>
          {Array.from(bundleCounts.entries())
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([bundle, count]) => {
              const selected = bundleFilter === bundle
              return (
                <button
                  key={bundle}
                  type="button"
                  onClick={() => setBundle(selected ? undefined : bundle)}
                  aria-pressed={selected}
 className={`text-cap px-2 py-1 rounded-sm flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
                    selected
                      ? 'border-accent bg-accent-bg text-accent'
                      : 'border-line hover:border-accent/60 text-ink'
                  }`}
                >
                  {bundle}
                  <span className="text-mute">{count}</span>
                </button>
              )
            })}
        </div>

        {/* Vertical divider + deps toggle */}
        <span className="h-5 w-px bg-line" aria-hidden="true" />

        <button
          type="button"
          onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
          aria-pressed={withDepsOnly}
          title={t('dict.list.deps.only')}
 className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
            withDepsOnly
              ? 'border-accent bg-accent-bg text-accent'
              : 'border-line hover:border-accent/60 text-ink'
          }`}
        >
          {t('dict.list.deps.short')}
        </button>
      </div>

      {/* Empty state */}
      {filtered.length === 0 ? (
        <div className="border border-line rounded-lg p-12 text-center bg-surface-2">
          <p className="font-sans text-title-md text-accent mb-2">
            {t('dict.list.search.empty')}
          </p>
          {filtersActive && (
            <Button type="button" variant="ghost" onClick={resetFilters}>
              {t('dict.list.search.reset')}
            </Button>
          )}
        </div>
      ) : (
        // Bundle-grouped 2-col card grid
        Array.from(grouped.entries()).map(([bundle, items]) => (
          <section key={bundle} className="space-y-3">
            <h2 className="flex items-baseline gap-3 pb-2 border-b border-line">
              <span className="text-body uppercase tracking-[0.10em] text-accent font-mono font-medium">
                {bundle}
              </span>
              <span className="text-cap text-mute">
                · {items.length} {t('dict.list.records.short')}
              </span>
            </h2>
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
              {items.map((d) => (
                <DictCard key={d.id} d={d} t={t} />
              ))}
            </div>
          </section>
        ))
      )}

      <DictionaryEditorDialog
        open={createOpen}
        mode={{ kind: 'create' }}
        onClose={() => setCreateOpen(false)}
        onSuccess={(name) => {
          setCreateOpen(false)
          navigate({ to: '/dictionaries/$name', params: { name } })
        }}
      />
    </section>
  )
}

// ===== Card =====

type TFunc = ReturnType<typeof useTranslation>['t']

const MAX_REFBY_CHIPS = 3

/**
 * Дедуплицирует SchemaDependent[] по `sourceDict` (одно название может
 * указывать через несколько fields — на катаоге показываем как один chip).
 * Сохраняет displayName из первого вхождения.
 */
export const uniqueRefBy = (deps: SchemaDependent[]): SchemaDependent[] => {
  const seen = new Set<string>()
  const out: SchemaDependent[] = []
  for (const dep of deps) {
    if (!seen.has(dep.sourceDict)) {
      seen.add(dep.sourceDict)
      out.push(dep)
    }
  }
  return out
}

const FkChip = ({
  to,
  label,
  dim,
}: {
  to: string
  label: string
  dim?: boolean
}) => (
  <Link
    to="/dictionaries/$name"
    params={{ name: to }}
    onClick={(e) => e.stopPropagation()}
    title={to}
    className={`inline-flex items-center px-[7px] py-[2px] rounded-[4px] font-mono text-[11px] transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
      dim
        ? 'border border-dashed border-line text-mute hover:border-accent/60 hover:text-accent'
        : 'bg-accent-bg text-accent hover:bg-accent hover:text-on-accent'
    }`}
  >
    {label}
  </Link>
)

const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
  // refBy — backend возвращает SchemaDependent[] (cached 5min, parallel
  // queries dedup'ятся TanStack Query'ем). На каталоге фетчим per dict, что на
  // 40 dicts = 40 параллельных GET — bursty первый раз, кешировано далее.
  // outgoing fk → ссылается потребует schemaJson per dict (отдельный followup,
  // backend batch endpoint желателен).
  const { data: refByRaw } = useDictionaryDependents(d.name)
  const refBy = useMemo(() => uniqueRefBy(refByRaw ?? []), [refByRaw])
  const hasFkRow = refBy.length > 0

  return (
    <Link
      to="/dictionaries/$name"
      params={{ name: d.name }}
      className="grid grid-cols-[3px_1fr_auto] bg-surface border border-line rounded-lg overflow-hidden transition-all hover:-translate-y-px hover:shadow-md hover:border-line-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
    >
      {/* Scope strip 3px вертикальная полоса (handoff spec) */}
      <span className={SCOPE_STRIP[d.scope]} aria-hidden="true" />

      {/* Body */}
      <div className="px-3.5 py-3 min-w-0">
        <div className="flex items-baseline gap-2.5 flex-wrap">
          <h3 className="text-title-md text-navy truncate">
            {d.displayName ?? d.name}
          </h3>
          <span className="text-mono text-mute truncate">{d.name}</span>
          {d.approvalRequired && (
            <Badge variant="warning">{t('dict.list.approval')}</Badge>
          )}
        </div>
        {d.description && (
          <p className="text-[12px] text-ink-2 mt-1 leading-snug line-clamp-2">
            {d.description}
          </p>
        )}
        {hasFkRow && (
          <div className="flex flex-wrap items-center gap-1.5 mt-2.5">
            <span className="text-cap font-display text-mute mr-1">
              {t('dict.list.fk.usedBy', { count: refBy.length })}
            </span>
            {refBy.slice(0, MAX_REFBY_CHIPS).map((dep) => (
              <FkChip
                key={dep.sourceDict}
                to={dep.sourceDict}
                label={dep.sourceDisplayName ?? dep.sourceDict}
                dim
              />
            ))}
            {refBy.length > MAX_REFBY_CHIPS && (
              <span className="text-mono text-mute">
                +{refBy.length - MAX_REFBY_CHIPS}
              </span>
            )}
          </div>
        )}
      </div>

      {/* Right rail: scope badge + version */}
      <div className="px-3.5 py-3 flex flex-col items-end gap-1.5 shrink-0">
        <Badge variant="info">{d.scope}</Badge>
        <span className="text-mono text-mute whitespace-nowrap">
          v{d.schemaVersion}
        </span>
        {typeof d.recordCount === 'number' && (
          <span className="text-cap font-display text-mute/80 whitespace-nowrap">
            {t('dict.list.recordCount', { count: d.recordCount })}
          </span>
        )}
      </div>
    </Link>
  )
}
