import axios from 'axios'

/**
 * Source of truth для access token. {@link TokenSync} обновляет это значение
 * синхронно при изменении OIDC user state. Interceptor читает из in-memory
 * holder'а — никаких {@code defaults.headers}, никаких {@code localStorage} poll'ов.
 *
 * <p>Зачем in-memory holder, не localStorage:
 * <ul>
 *   <li>Race-free: interceptor и TokenSync видят один и тот же token immediately
 *       после {@code setAccessToken}, без ожидания useEffect cycle.</li>
 *   <li>Logout: {@code setAccessToken(null)} → следующий request immediately
 *       без Authorization, без zombie-header в {@code apiClient.defaults}.</li>
 *   <li>Security: access token не сохраняется в {@code localStorage} (XSS
 *       persistent reach). См. также {@code oidcConfig.userStore} → sessionStorage.</li>
 * </ul>
 */
let accessToken: string | null = null

export const setAccessToken = (token: string | null): void => {
  accessToken = token
}

export const getAccessToken = (): string | null => accessToken

/**
 * Handler для 401 → silent renew или redirect. Регистрируется {@link TokenSync}
 * один раз. Все 401 проходят через единственный interceptor ниже —
 * двойной handler (как раньше: один в client.ts, второй в TokenSync) приводил
 * к race signinSilent vs signinRedirect и redirect loop.
 */
export type Unauthorized401Handler = (error: unknown) => void
let unauthorizedHandler: Unauthorized401Handler | null = null

export const setUnauthorizedHandler = (h: Unauthorized401Handler | null): void => {
  unauthorizedHandler = h
}

/**
 * baseURL построение:
 * - Если VITE_API_BASE задан и заканчивается на /api/v1 (или содержит api/) —
 *   используем как есть.
 * - Если VITE_API_BASE задан без api-prefix (e.g. https://api.example.com) —
 *   автоматически добавляем `/api/v1` суффикс.
 * - Default: `/api/v1` (relative — admin-ui nginx proxies to writer/read-api).
 *
 * Раньше ambiguous — если кто-то задал `VITE_API_BASE=https://api.example.com`
 * без `/v1`, все запросы становились `/dictionaries` относительно корня
 * (правильно `/api/v1/dictionaries`). Теперь fall-through к expected layout.
 */
const buildBaseURL = (): string => {
  const env = import.meta.env.VITE_API_BASE
  if (!env) return '/api/v1'
  const trimmed = env.replace(/\/+$/, '')
  if (/\/api(\/|$)/i.test(trimmed)) return trimmed
  return `${trimmed}/api/v1`
}

export const apiClient = axios.create({
  baseURL: buildBaseURL(),
  timeout: 10_000,
})

apiClient.interceptors.request.use((config) => {
  const lang = (typeof navigator !== 'undefined' && navigator.language) || 'ru-RU'
  config.headers['Accept-Language'] = `${lang},ru-RU;q=0.9,en-US;q=0.8`
  if (accessToken) {
    config.headers['Authorization'] = `Bearer ${accessToken}`
  } else {
    delete config.headers['Authorization']
  }
  return config
})

// Единственный 401 interceptor. Логика silent renew / redirect / loop guard
// делегируется handler'у который TokenSync устанавливает через
// setUnauthorizedHandler().
apiClient.interceptors.response.use(
  (resp) => resp,
  (error) => {
    if (error?.response?.status === 401 && unauthorizedHandler) {
      try {
        unauthorizedHandler(error)
      } catch (e) {
        console.error('[api] unauthorized handler failed', e)
      }
    }
    return Promise.reject(error)
  },
)

export type DataScope = 'PUBLIC' | 'INTERNAL' | 'RESTRICTED'

export type DictionaryDefinition = {
  id: string
  name: string
  displayName?: string
  description?: string
  scope: DataScope
  schemaVersion: string
  bundle: string
  supportedLocales: string[]
  defaultLocale: string
  /** CEO plan E2: per-dict Redis projection materialization opt-in. */
  redisProjectionEnabled: boolean
  /** Approval Workflow v2: per-dict opt-in. */
  approvalRequired: boolean
  approvalMinRole?: string | null
  recordCount?: number
  createdAt: string
  updatedAt: string
}

export type JsonSchema = {
  $schema?: string
  type?: string
  title?: string
  description?: string
  properties?: Record<string, JsonSchema>
  required?: string[]
  enum?: unknown[]
  format?: string
  pattern?: string
  minLength?: number
  maxLength?: number
  minimum?: number
  maximum?: number
  items?: JsonSchema
  additionalProperties?: boolean | JsonSchema
  ['x-localized']?: boolean
  ['x-references']?: string
  ['x-id-source']?: string
  ['x-unique']?: boolean
  /**
   * Section bucket for form rendering. Если задан — поле попадает в эту
   * секцию (можно использовать predefined ID: identity / references /
   * description / dates / geometry / physical / extra; или любой custom
   * string — он отобразится с humanized fallback меткой).
   *
   * Без `x-section` SchemaDrivenForm.deriveSection auto-derive'ит секцию
   * по эвристикам (FK→references, localized→description, format=date→dates,
   * geometry-like keys→geometry, required→identity, else→extra).
   */
  ['x-section']?: string
  default?: unknown
}

export type DictionaryDetail = DictionaryDefinition & {
  schemaJson: JsonSchema
}

export type FlattenedRecord = {
  id: string
  businessKey: string
  data: Record<string, unknown>
  dataScope: DataScope
  validFrom: string
  validTo: string
  _meta?: { locale?: string }
}

export type RecordResponse = {
  id: string
  dictionaryId: string
  businessKey: string
  data: Record<string, unknown>
  geometryWkt?: string
  dataScope: DataScope
  validFrom: string
  validTo: string
  createdAt: string
  updatedAt: string
  createdBy?: string
  updatedBy?: string
  version: number
}

export type CreateDictionaryRequest = {
  name: string
  displayName?: string
  description?: string
  scope: DataScope
  schemaJson: JsonSchema
  schemaVersion?: string
  bundle?: string
  supportedLocales?: string[]
  defaultLocale?: string
  /** CEO plan E2: per-dict Redis projection opt-in. Default false. */
  redisProjectionEnabled?: boolean
  /** Approval Workflow v2: per-dict opt-in. Default false. */
  approvalRequired?: boolean
  /** Optional Keycloak role для reviewer'ов на этот dict. */
  approvalMinRole?: string
}

export type CreateRecordRequest = {
  businessKey: string
  data: Record<string, unknown>
  geometryWkt?: string
  validFrom?: string
  validTo?: string
}

export type BulkCloseRequest = {
  businessKeys: string[]
  at?: string
  reason?: string
}

export type BulkCloseResponse = {
  closed: string[]
  skipped: { businessKey: string; reason: string }[]
  errors: { businessKey: string; reason: string }[]
}

export type AuditAction = 'CREATE' | 'UPDATE' | 'CLOSE'

export type AuditEntry = {
  id: number
  eventTime: string
  eventType: string
  action: AuditAction | string
  userId?: string | null
  userScope?: DataScope | null
  dictionaryName?: string | null
  dictionaryId?: string | null
  recordId?: string | null
  businessKey?: string | null
  payloadBefore?: Record<string, unknown> | null
  payloadAfter?: Record<string, unknown> | null
  traceId?: string | null
  requestId?: string | null
  ipAddress?: string | null
  userAgent?: string | null
}

export type AuditPage = {
  content: AuditEntry[]
  totalElements: number
  totalPages: number
  number: number
  size: number
  first: boolean
  last: boolean
}

export type AuditFilters = {
  dictionaryName?: string
  userId?: string
  action?: AuditAction | ''
  businessKey?: string
  from?: string
  to?: string
  page?: number
  size?: number
}

export type OutboxStats = { pending: number; dlq: number }

export type DlqEntry = {
  id: number
  eventType: string
  dictionaryName?: string | null
  aggregateId: string
  kafkaTopic: string
  attemptCount: number
  lastError?: string | null
  createdAt: string
  dlqAt: string
}

export type DlqPage = {
  content: DlqEntry[]
  totalElements: number
  totalPages: number
  number: number
  size: number
}

// === Webhooks (v2) ===

export type WebhookSubscription = {
  id: string
  name: string
  url: string
  scopeFilter?: DataScope[] | null
  dictionaryFilter?: string[] | null
  eventTypeFilter?: string[] | null
  /** Masked `sk_****<last4>` или plaintext только в response создания/rotate. */
  hmacSecret: string
  active: boolean
  createdAt: string
  createdBy: string
  updatedAt: string
  description?: string | null
}

export type CreateWebhookSubscriptionRequest = {
  name: string
  url: string
  scopeFilter?: DataScope[]
  dictionaryFilter?: string[]
  eventTypeFilter?: string[]
  active?: boolean
  description?: string
}

export type WebhookDeliveryStatus = 'pending' | 'retrying' | 'delivered' | 'dlq'

export type WebhookDelivery = {
  id: number
  subscriptionId: string
  outboxEventId: number
  status: WebhookDeliveryStatus
  attemptCount: number
  createdAt: string
  nextAttemptAt: string
  lastAttemptAt?: string | null
  deliveredAt?: string | null
  lastError?: string | null
  lastStatusCode?: number | null
  dlqAt?: string | null
}

export type WebhookDeliveryPage = {
  content: WebhookDelivery[]
  totalElements: number
  totalPages: number
  number: number
  size: number
}

/** Один bucket histogram time-series stats. ISO ts + counts по status. */
export type WebhookStatsBucket = {
  ts: string
  pending: number
  retrying: number
  delivered: number
  dlq: number
  total: number
}

/** Response от GET /admin/webhooks/subscriptions/{id}/stats. */
export type WebhookDeliveryStats = {
  bucketBy: 'hour' | 'day'
  from: string
  to: string
  buckets: WebhookStatsBucket[]
}

// === Approval Workflow v2 ===

export type DraftStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'WITHDRAWN'
export type DraftOperation = 'CREATE' | 'UPDATE' | 'CLOSE'

export type DraftResponse = {
  id: string
  dictionaryId: string
  businessKey: string
  parentRecordId?: string | null
  operation: DraftOperation
  data?: Record<string, unknown> | null
  geometryWkt?: string | null
  proposedValidFrom?: string | null
  proposedValidTo?: string | null
  status: DraftStatus
  makerId: string
  makerComment?: string | null
  submittedAt: string
  reviewerId?: string | null
  reviewedAt?: string | null
  reviewComment?: string | null
}

export type SubmitDraftRequest = {
  businessKey: string
  operation: DraftOperation
  data?: Record<string, unknown> | null
  geometryWkt?: string | null
  validFrom?: string | null
  validTo?: string | null
  comment?: string | null
}

export type ReviewQueuePage = {
  items: DraftResponse[]
  page: number
  size: number
  totalElements: number
  totalPages: number
}

// === Smart search (CEO plan v1) ===

export type SearchItem = {
  businessKey: string
  dataScope: string
  createdAt: string
}

export type SearchDictGroup = {
  dictName: string
  dictDisplayName?: string | null
  count: number
  items: SearchItem[]
}

export type SearchResponse = {
  query: string
  total: number
  groups: SearchDictGroup[]
}

// === Dependents (Phase 1 dict-relationships-v2) ===

export type OnCloseAction = 'BLOCK' | 'WARN' | 'CASCADE'

export type SchemaDependent = {
  sourceDict: string
  sourceDisplayName?: string | null
  sourceField: string
  targetField: string
  onClose: OnCloseAction
  /** Total active records in source dict (для UI badge "X записей всего"). */
  activeRecordsInSourceDict: number
}

export type RecordDependent = {
  sourceDict: string
  sourceDisplayName?: string | null
  sourceField: string
  onClose: OnCloseAction
  id: string
  businessKey: string
  validFrom: string
  validTo: string
  createdAt: string
}

export type PerSourceSummary = {
  sourceDict: string
  sourceDisplayName?: string | null
  sourceField: string
  onClose: OnCloseAction
  count: number
}

export type LineageMeta = {
  /** True если backend использует materialized view; false — JSONB direct (fresh). */
  mvEnabled: boolean
  /** ISO timestamp последнего успешного MV refresh; null если MV выключена / never. */
  lastRefreshedAt: string | null
  /** "ok" | "error" | "never" — null если mvEnabled=false. */
  lastStatus: string | null
  lastDurationMs: number | null
}

export type CascadeEntry = {
  sourceDict: string
  sourceDisplayName?: string | null
  businessKey: string
  sourceField: string
  onClose: OnCloseAction
  recordId: string
}

export type CascadePlan = {
  targetDict: string
  targetKey: string
  blockers: CascadeEntry[]
  warnings: CascadeEntry[]
  cascade: CascadeEntry[]
  totalDependents: number
}

export type CascadeCloseResult = {
  targetDict: string
  targetKey: string
  cascadedRecords: CascadeEntry[]
  warnings: CascadeEntry[]
  totalClosed: number
}

export type RecordDependentsPage = {
  targetDict: string
  targetBusinessKey: string
  items: RecordDependent[]
  perSource: PerSourceSummary[]
  total: number
  page: number
  size: number
  /** Phase 2 dict-relationships-v2: backing data source metadata + staleness. */
  lineageMeta?: LineageMeta | null
}

export type WebhookTestPingResult = {
  /** success | failure | rejected */
  status: 'success' | 'failure' | 'rejected'
  httpStatus?: number | null
  latencyMs?: number | null
  message?: string | null
}

// === Changelog (v2.4.0 + full timeline + diff) ===

/**
 * Frontend-friendly event kind. Backend mapping см.
 * ChangelogService.mapEventTypeToKind. Используем для icon/color/label
 * выбора в History tab. Versioning events
 * (definition_create / definition_update / draft_publish) показываются
 * с version label; promotional draft events идут без version.
 */
export type ChangelogKind =
  | 'definition_create'
  | 'definition_update'
  | 'draft_create'
  | 'draft_update'
  | 'draft_review'
  | 'draft_approve'
  | 'draft_reject'
  | 'draft_changes_requested'
  | 'draft_publish'
  | 'draft_withdraw'

export type ChangelogPublisher = {
  /** JWT sub claim (Keycloak user id). */
  sub: string
  /** Display name. Backend пока эхает sub; готовим место под user-resolve. */
  name: string
}

export type ChangelogDiffSummary = {
  added: string[]
  removed: string[]
  changed: string[]
}

export type ChangelogEntry = {
  /** audit_log.id — deep-link на diff endpoint. */
  id: number
  /** Semver. null для promotional draft events. */
  version: string | null
  publishedAt: string
  publishedBy: ChangelogPublisher
  /** Header summary text для card UI. */
  headers: string
  diff: ChangelogDiffSummary
  recordsAffected: number
  isCurrent: boolean
  kind: ChangelogKind
}

export type ChangelogResponse = {
  dictionary: string
  currentVersion: string
  entries: ChangelogEntry[]
}

/**
 * Full before/after schemas для side-by-side render. before=null для
 * definition_create. Для draft_* (не publish) — after=proposed_schema,
 * before=live schema на момент event'а (то что предлагалось vs реальность).
 */
// === Schema workflow (maker-checker drafts, v2.2.0 backend) ===

/**
 * Server возвращает status в lowercase ({@code "draft"}, {@code "review_pending"},
 * ...) — match'им backend dbValue() напрямую, без перевода в frontend.
 *
 * Terminal: REJECTED, PUBLISHED, WITHDRAWN.
 * Active: DRAFT, REVIEW_PENDING, APPROVED, CHANGES_REQUESTED.
 */
export type SchemaDraftStatus =
  | 'draft'
  | 'review_pending'
  | 'approved'
  | 'changes_requested'
  | 'rejected'
  | 'published'
  | 'withdrawn'

export const TERMINAL_SCHEMA_DRAFT_STATUSES: ReadonlySet<SchemaDraftStatus> = new Set([
  'rejected',
  'published',
  'withdrawn',
])

export const isActiveSchemaDraft = (s: SchemaDraftStatus): boolean =>
  !TERMINAL_SCHEMA_DRAFT_STATUSES.has(s)

export type SchemaDraft = {
  draftId: string
  dictionaryId: string
  dictionaryName: string
  status: SchemaDraftStatus
  branchedFromVersion: string
  /** Full proposed JSON Schema. */
  proposedSchema: unknown
  reason?: string | null
  makerId: string
  createdAt: string
  submittedAt?: string | null
  /** Optional reviewer list (NULL = open pool). */
  reviewers?: unknown | null
  reviewNote?: string | null
  decisionUserId?: string | null
  decisionAt?: string | null
  decisionComment?: string | null
  publishedAt?: string | null
  publishedVersion?: string | null
  publishNote?: string | null
  /** JPA optimistic lock counter. */
  version: number
}

export type CreateSchemaDraftRequest = {
  /** Current HEAD schema version this draft branches from. */
  fromVersion: string
  reason?: string
  /** Full proposed JSON Schema (replace, not patch). */
  proposedSchema: unknown
}

/**
 * Partial update existing draft без recreate. Status-gate enforced
 * backend'ом: только DRAFT и CHANGES_REQUESTED. CHANGES_REQUESTED →
 * DRAFT auto-transition после update (maker должен ре-submit).
 */
export type UpdateSchemaDraftRequest = {
  /** Replacement JSON Schema (full document). undefined = не меняем. */
  proposedSchema?: unknown
  /** Новая maker rationale. undefined = не меняем. */
  reason?: string
}

export type SubmitForReviewRequest = {
  /** Optional explicit reviewer IDs. NULL/[] = pool. */
  reviewers?: string[] | null
  note?: string | null
}

export type SchemaDraftVerdict = 'APPROVE' | 'REQUEST_CHANGES' | 'REJECT'

export type DecisionRequest = {
  decision: SchemaDraftVerdict
  comment?: string
}

export type PublishSchemaDraftRequest = {
  /** Optimistic lock — current schema version от UI. */
  expectedHeadVersion: string
  publishNote?: string
}

/** Response для POST /publish — wraps draft + computed bump. */
export type PublishSchemaDraftResult = {
  draft: SchemaDraft
  newVersion: string
  publishedAt: string
}

/** Pool queue для /admin/schema-reviews/pending.
 *
 * <p>Backend сериализует Spring {@code Page<SchemaDraft>} как:
 * {@code {items, page, size, totalElements, totalPages}}. Раньше type
 * объявлял {@code total} что было mismatched — consumers (`reviews.tsx`,
 * `useReviewsBadgeCount`) читали {@code data.total} который === undefined,
 * fallback'или на {@code items.length} (или 0 для badge — отсюда баг
 * "бэйджа нет"). Type совпадает с фактическим контрактом backend'а. */
export type SchemaReviewQueuePage = {
  items: SchemaDraft[]
  page: number
  size: number
  totalElements: number
  totalPages: number
}

export type ChangelogDiff = {
  id: number
  dictionaryName: string
  /** Raw audit event_type (SCHEMA_UPDATED / SCHEMA_DRAFT_PUBLISHED / ...). */
  eventType: string
  kind: ChangelogKind
  occurredAt: string
  publishedBy: ChangelogPublisher
  before: unknown | null
  after: unknown | null
  summary: ChangelogDiffSummary
}
