import { createContext, useContext } from 'react' import { languageUIMessage, translateUIMessage } from '@/lib/i18n' import { apiGet } from '@/lib/api' import type { LocaleCode } from '@/lib/locales' import type { CountField, CountsResource } from '@/lib/counts' export type CountsAxis = { field: string; grain?: string } export type CountsCondition = { field: string; operator: string; grain?: string; value?: string | number | boolean | null; end?: string; blank?: boolean input?: 'single' | 'multiple' | 'list'; values?: (string | number | boolean)[] } export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum' | 'json' | 'boolean' | 'interval' export const countsResources: CountsResource[] = [ 'posts', 'tags', 'nico_tags', 'tag_descriptions', 'tag_groups', 'materials'] // Navigation visibility only; supported resources and direct URLs remain available. // The external-tag entry currently uses Nico tag history. export const visibleCountsResources = [ 'posts', 'tags', 'nico_tags'] as const satisfies readonly CountsResource[] export type CountsFieldMetadata = { key: string; type: CountsFieldType; nullable: boolean; operators: string[] labels?: string[]; multiple?: boolean; candidateSource?: 'categories' | 'tags' setType?: 'tag_reference'; inputTypes?: string[] labelValues?: Record nameLocale?: CountField['nameLocale'] legacy?: boolean choices?: { value: string; label: string }[] } export type CountsDefinition = { fields: CountsFieldMetadata[] outputFields?: CountField[] nameColumns?: CountField[] columnAliases?: { key: string; value: string }[] conditionAliases?: { key: string; value: string }[] filterFields?: CountsFieldMetadata[] defaults?: { definition: string; mode: string; params: { key: string; value: string }[] }[] axes: { latest: string[]; history: string[] } grains: string[] } export const CountsDefinitionContext = createContext (null) export const useCountsDefinition = (): CountsDefinition => { const definition = useContext (CountsDefinitionContext) if (definition == null) throw new Error ('Missing Counts definition') return definition } const localiseCountsDefinition = ( definition: CountsDefinition, locale: LocaleCode, ): CountsDefinition => { const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => { if (name == null) return null const language = languageUIMessage (name.languageCode, locale).text const label = aliasesFlg ? 'counts:localeAliases' : 'counts:localeName' return { label: name.scriptCode == null ? label : `${label}WithScript`, labelValues: { language, script: name.scriptCode ?? '' } } } const columns = (fields: CountField[]): CountField[] => fields.map (field => ({ ...field, ...heading (field.nameLocale, field.multipleValues === true), ...(field.children == null ? { } : { children: columns (field.children) }) })) const metadata = (fields: CountsFieldMetadata[]): CountsFieldMetadata[] => fields.map (field => { const aliasesFlg = field.key.split ('.').pop()?.startsWith ('aliases_') === true const name = heading (field.nameLocale, aliasesFlg) return name == null ? field : { ...field, labelValues: name.labelValues, labels: field.labels?.map (label => ['counts:localeName', 'counts:localeAliases'].includes (label) ? name.label : label) } }) return { ...definition, fields: metadata (definition.fields), ...(definition.outputFields == null ? { } : { outputFields: columns (definition.outputFields) }), ...(definition.nameColumns == null ? { } : { nameColumns: columns (definition.nameColumns) }), ...(definition.filterFields == null ? { } : { filterFields: metadata (definition.filterFields) }) } } export const fetchCountsDefinition = ( resource: CountsResource, locale: LocaleCode = 'ja', ): Promise => apiGet ( `/counts/definitions/${encodeURIComponent (resource)}`, { locale }) .then (definition => localiseCountsDefinition (definition, locale)) export type CountsCandidate = { value: string; id?: number; name: string | null; category?: string | null names?: Record kind?: 'internal' | 'external' } export type CountsCandidatesResult = { rows: CountsCandidate[]; page: number; more: boolean; executionAt: string } export const fetchCountsCandidates = ( params: URLSearchParams, locale: LocaleCode, signal?: AbortSignal, ): Promise => apiGet ( '/counts/definitions/posts/candidates', { params: Object.fromEntries (params), locale, signal }) export const countsSetOperators = ['in', 'not_in', 'any', 'all', 'none'] export const countsConditionInputs = ( field: CountsFieldMetadata | undefined, operator: string, ): ('single' | 'multiple' | 'list')[] => { if (!countsSetOperators.includes (operator)) return ['single'] if (field?.candidateSource === 'tags') return ['list'] return field?.candidateSource == null ? ['multiple'] : ['list', 'multiple'] } export const countsFilterFields = ( definition: CountsDefinition, ): Record => Object.fromEntries ( (definition.filterFields ?? definition.fields).map (field => [field.key, field.type])) export const countsFieldLabel = (key: string): string => { const labels: Record = { resource_id: 'columns.resourceId', version_no: 'columns.version', user: 'history:actor', created_at: 'columns.changedAt', name: 'history:name', title: 'columns.postTitle', url: 'columns.postURL', thumbnail_base: 'columns.thumbnailURL', tags: 'columns.tags', tags_json: 'columns.tags', names: 'columns.names', aliases: 'tags:aliases', category: 'common:categoryLabel', language_code: 'columns.language', source_language_code: 'attributes.sourceLanguage', changed_field: 'changedField' } return labels[key] ?? `attributes.${key}` } export const countsEventTypes = ['create', 'update', 'discard', 'restore'] as const export const countsAxisLabel = ( axis: CountsAxis, definition: CountsDefinition, locale: LocaleCode, ): { text: string; language: string } => { const field = translateUIMessage (countsFieldLabel (axis.field), 'counts', locale) if (definition.fields.find (item => item.key === axis.field)?.type !== 'datetime') return field return translateUIMessage ('axisWithGrain', 'counts', locale, { field: field.text, grain: translateUIMessage (`grains.${axis.grain ?? 'month'}`, 'counts', locale).text }) } export const countsAxisFields = ( definition: CountsDefinition, history: boolean, ): CountField[] => definition.axes[history ? 'history' : 'latest'] .map (key => ({ key, label: countsFieldLabel (key) }))