import type { JsonSchema } from '@/api/client'

export type PropertyKind =
  | 'string'
  | 'integer'
  | 'number'
  | 'boolean'
  | 'enum'
  | 'reference' // FK на запись другого справочника через x-references
  | 'localized'
  | 'date'
  | 'datetime'
  | 'array_string'
  | 'opaque' // Сложный тип (nested object, array of objects), редактируется только через JSON-таб

export type PropertyDef = {
  id: string
  name: string
  kind: PropertyKind
  required: boolean
  unique: boolean
  description?: string
  enumValues?: string[]
  minLength?: number
  maxLength?: number
  pattern?: string
  minimum?: number
  maximum?: number
  uniqueItems?: boolean
  /** Для kind: 'reference' — "dict_name.field_name" (например "spacecraft_status.code").
   *  Сериализуется как `x-references` в JSON Schema; ReferenceValidator на backend
   *  проверяет FK на CRUD. */
  referenceTarget?: string
  /** Для kind: 'opaque' — оригинальный JSON Schema fragment, передаётся
   *  без изменений на save. Защита от lossy round-trip при nested objects. */
  rawSchema?: JsonSchema
}

export const PROPERTY_KINDS: PropertyKind[] = [
  'string',
  'integer',
  'number',
  'boolean',
  'enum',
  'reference',
  'localized',
  'date',
  'datetime',
  'array_string',
]

const newId = () =>
  typeof crypto !== 'undefined' && 'randomUUID' in crypto
    ? crypto.randomUUID()
    : `${Date.now()}-${Math.random().toString(36).slice(2)}`

export const newPropertyDef = (): PropertyDef => ({
  id: newId(),
  name: '',
  kind: 'string',
  required: false,
  unique: false,
})

export const buildSchemaJson = (
  props: PropertyDef[],
  idSource?: string,
): JsonSchema => {
  const properties: Record<string, JsonSchema> = {}
  const required: string[] = []
  for (const p of props) {
    if (!p.name) continue
    properties[p.name] = propertyToSchema(p)
    if (p.required) required.push(p.name)
  }
  const schema: Record<string, unknown> = {
    $schema: 'http://json-schema.org/draft-07/schema#',
    type: 'object',
    additionalProperties: false,
    required: required.length > 0 ? required : undefined,
    properties,
  }
  if (idSource) schema['x-id-source'] = idSource
  return schema as JsonSchema
}

const propertyToSchema = (p: PropertyDef): JsonSchema => {
  const desc = p.description?.trim() || undefined
  const uniqueExt = p.unique ? { 'x-unique': true } : {}

  switch (p.kind) {
    case 'string': {
      const s: JsonSchema = { type: 'string', description: desc, ...uniqueExt }
      if (p.minLength != null) s.minLength = p.minLength
      if (p.maxLength != null) s.maxLength = p.maxLength
      if (p.pattern) s.pattern = p.pattern
      return s
    }
    case 'integer':
    case 'number': {
      const s: JsonSchema = { type: p.kind, description: desc, ...uniqueExt }
      if (p.minimum != null) s.minimum = p.minimum
      if (p.maximum != null) s.maximum = p.maximum
      return s
    }
    case 'boolean':
      return { type: 'boolean', description: desc, ...uniqueExt }
    case 'enum':
      return {
        type: 'string',
        description: desc,
        enum: p.enumValues ?? [],
        ...uniqueExt,
      }
    case 'reference':
      // x-references = "dict.field". Backend ReferenceValidator парсит и
      // проверяет FK на CRUD. Если referenceTarget пуст — emit пустую
      // string ссылку (validate ловит на save), но это invalid bundle —
      // UI должна disable Save кнопку при empty referenceTarget.
      return {
        type: 'string',
        description: desc,
        ['x-references']: p.referenceTarget ?? '',
        ...uniqueExt,
      } as JsonSchema
    case 'localized':
      return {
        type: 'object',
        description: desc,
        ['x-localized']: true,
        ...uniqueExt,
        ...({
          patternProperties: {
            '^[a-z]{2}-[A-Z]{2}$': { type: 'string' },
          },
        } as object),
      }
    case 'date':
      return { type: 'string', format: 'date', description: desc, ...uniqueExt }
    case 'datetime':
      return { type: 'string', format: 'date-time', description: desc, ...uniqueExt }
    case 'array_string':
      return {
        type: 'array',
        description: desc,
        items: p.enumValues && p.enumValues.length > 0
          ? ({ type: 'string', enum: p.enumValues } as JsonSchema)
          : { type: 'string' },
        ...(p.uniqueItems ? { uniqueItems: true } : {}),
        ...uniqueExt,
      } as JsonSchema
    case 'opaque':
      // Round-trip raw schema без изменений. Description можно overwrite
      // если admin отредактировал в metadata-табе, остальное as-is.
      return p.rawSchema
        ? (desc !== undefined && desc !== p.rawSchema.description
            ? { ...p.rawSchema, description: desc }
            : p.rawSchema)
        : { type: 'string', description: desc }
  }
}

export const parseSchemaJson = (
  schema: JsonSchema | undefined,
): { properties: PropertyDef[]; idSource: string | undefined } => {
  if (!schema || !schema.properties) {
    return { properties: [], idSource: (schema as { 'x-id-source'?: string } | undefined)?.['x-id-source'] }
  }
  const required = new Set(schema.required ?? [])
  const list: PropertyDef[] = []
  for (const [name, raw] of Object.entries(schema.properties)) {
    const def = inferKind(name, raw, required.has(name))
    if (def) list.push(def)
  }
  const idSource = (schema as { 'x-id-source'?: string })['x-id-source']
  return { properties: list, idSource }
}

const inferKind = (
  name: string,
  raw: JsonSchema,
  isRequired: boolean,
): PropertyDef | null => {
  const unique = Boolean((raw as { 'x-unique'?: boolean })['x-unique'])
  const base = {
    id: newId(),
    name,
    required: isRequired,
    unique,
    description: raw.description,
  }

  if (raw['x-localized']) return { ...base, kind: 'localized' }
  if (raw.enum) return { ...base, kind: 'enum', enumValues: raw.enum.map(String) }
  // FK через x-references — приоритет над plain string fallback. После
  // round-trip останется reference kind, не теряется breaking change detection.
  if (typeof raw['x-references'] === 'string' && raw['x-references']) {
    return { ...base, kind: 'reference', referenceTarget: raw['x-references'] }
  }
  if (raw.type === 'string' && raw.format === 'date') return { ...base, kind: 'date' }
  if (raw.type === 'string' && raw.format === 'date-time') return { ...base, kind: 'datetime' }

  if (raw.type === 'string') {
    return {
      ...base,
      kind: 'string',
      minLength: raw.minLength,
      maxLength: raw.maxLength,
      pattern: raw.pattern,
    }
  }
  if (raw.type === 'integer' || raw.type === 'number') {
    return {
      ...base,
      kind: raw.type,
      minimum: raw.minimum,
      maximum: raw.maximum,
    }
  }
  if (raw.type === 'boolean') return { ...base, kind: 'boolean' }
  if (raw.type === 'array' && raw.items?.type === 'string') {
    const enumValues = (raw.items.enum as unknown[] | undefined)?.map(String)
    return {
      ...base,
      kind: 'array_string',
      enumValues,
      uniqueItems: Boolean((raw as { uniqueItems?: boolean }).uniqueItems),
    }
  }
  // Fallback: nested objects, complex arrays, unknown types — preserve raw
  // schema полностью. Lossy round-trip (object → string fallback) ломал
  // breaking-change detector + терял nested properties (см. spacecraft.orbit).
  return { ...base, kind: 'opaque', rawSchema: raw }
}

// === schema diff (breaking detection) ===

export type SchemaChange = {
  severity: 'breaking' | 'compatible' | 'metadata'
  field: string
  reason: string
}

const kindOf = (s: JsonSchema): string => {
  if (s['x-localized']) return 'localized'
  if (s.enum) return 'enum'
  // reference — отдельный kind для diff: смена FK target → breaking change,
  // не должен путаться со swap'ом обычной string ↔ reference.
  if (typeof s['x-references'] === 'string' && s['x-references']) return 'reference'
  if (s.type === 'string' && s.format === 'date') return 'date'
  if (s.type === 'string' && s.format === 'date-time') return 'datetime'
  return s.type ?? 'unknown'
}

export const diffSchemas = (
  prev: JsonSchema | undefined,
  next: JsonSchema,
): SchemaChange[] => {
  const changes: SchemaChange[] = []
  if (!prev) return changes // create — нет с чем сравнивать

  const prevProps = prev.properties ?? {}
  const nextProps = next.properties ?? {}
  const prevReq = new Set(prev.required ?? [])
  const nextReq = new Set(next.required ?? [])

  for (const name of Object.keys(prevProps)) {
    if (!(name in nextProps)) {
      changes.push({ severity: 'breaking', field: name, reason: 'removed' })
      continue
    }
    const a = prevProps[name]
    const b = nextProps[name]
    const ka = kindOf(a)
    const kb = kindOf(b)
    if (ka !== kb) {
      changes.push({ severity: 'breaking', field: name, reason: `type ${ka} → ${kb}` })
      continue
    }

    const wasReq = prevReq.has(name)
    const isReq = nextReq.has(name)
    if (!wasReq && isReq) {
      changes.push({ severity: 'breaking', field: name, reason: 'now required' })
    } else if (wasReq && !isReq) {
      changes.push({ severity: 'compatible', field: name, reason: 'now optional' })
    }

    const aEnum = (a.enum ?? []) as unknown[]
    const bEnum = (b.enum ?? []) as unknown[]
    if (aEnum.length > 0 || bEnum.length > 0) {
      const removed = aEnum.filter((v) => !bEnum.includes(v))
      const added = bEnum.filter((v) => !aEnum.includes(v))
      if (removed.length > 0) {
        changes.push({
          severity: 'breaking',
          field: name,
          reason: `enum value removed: ${removed.join(', ')}`,
        })
      }
      if (added.length > 0) {
        changes.push({
          severity: 'compatible',
          field: name,
          reason: `enum value added: ${added.join(', ')}`,
        })
      }
    }

    if (a.maxLength != null && b.maxLength != null && b.maxLength < a.maxLength) {
      changes.push({
        severity: 'breaking',
        field: name,
        reason: `maxLength tightened ${a.maxLength} → ${b.maxLength}`,
      })
    }
    if (a.minimum != null && b.minimum != null && b.minimum > a.minimum) {
      changes.push({
        severity: 'breaking',
        field: name,
        reason: `minimum tightened ${a.minimum} → ${b.minimum}`,
      })
    }
    if (a.maximum != null && b.maximum != null && b.maximum < a.maximum) {
      changes.push({
        severity: 'breaking',
        field: name,
        reason: `maximum tightened ${a.maximum} → ${b.maximum}`,
      })
    }
    if (a.pattern && b.pattern && a.pattern !== b.pattern) {
      changes.push({
        severity: 'breaking',
        field: name,
        reason: `pattern changed (${a.pattern} → ${b.pattern})`,
      })
    }

    if ((a.description ?? '') !== (b.description ?? '')) {
      changes.push({ severity: 'metadata', field: name, reason: 'description changed' })
    }
  }

  for (const name of Object.keys(nextProps)) {
    if (!(name in prevProps)) {
      const isReq = nextReq.has(name)
      changes.push({
        severity: isReq ? 'breaking' : 'compatible',
        field: name,
        reason: isReq ? 'new required field' : 'new optional field',
      })
    }
  }

  return changes
}

export const hasBreakingChanges = (changes: SchemaChange[]): boolean =>
  changes.some((c) => c.severity === 'breaking')

export const suggestVersionBump = (
  current: string | undefined,
  changes: SchemaChange[],
): string => {
  const v = parseVersion(current ?? '1.0.0')
  if (changes.length === 0) return formatVersion(v)
  if (hasBreakingChanges(changes)) return formatVersion({ ...v, major: v.major + 1, minor: 0, patch: 0 })
  const onlyMetadata = changes.every((c) => c.severity === 'metadata')
  if (onlyMetadata) return formatVersion({ ...v, patch: v.patch + 1 })
  return formatVersion({ ...v, minor: v.minor + 1, patch: 0 })
}

const parseVersion = (s: string): { major: number; minor: number; patch: number } => {
  const m = /^(\d+)\.(\d+)\.(\d+)/.exec(s)
  if (!m) return { major: 1, minor: 0, patch: 0 }
  return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }
}

const formatVersion = (v: { major: number; minor: number; patch: number }): string =>
  `${v.major}.${v.minor}.${v.patch}`
