import { FieldValidationError } from './errors.js'
import type { AnyFieldDefinition, FieldKey, FieldStorageValue, FieldType } from './schema.js'
import { createBuiltinFieldTypeDescriptors } from './values.js'

export interface DefinitionLimits { maxBytes: number; maxDepth: number; maxChildren: number }
export interface ValueLimits { maxBytes: number; maxDepth: number; maxNodes: number }
export interface FieldHeadlessSchema { type: string; nullable: boolean; readOnly?: boolean; constraints: Readonly<Record<string, unknown>> }
export type RevisionFieldValue = FieldStorageValue | { configured: boolean }

const sanitizedFieldHtmlBrand: unique symbol = Symbol('sanitized-field-html') as never
export interface SanitizedFieldHtml { readonly html: string; readonly [sanitizedFieldHtmlBrand]: true }
export type FieldPresentation<T> =
  | { readonly kind: 'text'; readonly value: string }
  | { readonly kind: 'structured'; readonly value: T }
  | { readonly kind: 'sanitizedHtml'; readonly value: SanitizedFieldHtml }

export interface FieldsPrincipal {
  readonly id: string
  readonly capabilities: ReadonlySet<string>
  readonly system?: boolean
}
export type FieldsAction = 'read' | 'write' | 'delete' | 'manageDefinitions' | 'migrateDefinitions' | 'readTarget' | 'writeReverseTarget'
export interface FieldsAuthorizedQueryScopeLike {
  readonly policyVersion: string
  applyTo<T>(query: T): T
}
export interface FieldsAuthorization {
  assert(principal: FieldsPrincipal, action: FieldsAction, ref: { entityType: string; entityId: string }): Promise<void> | void
  assertDefinitionMigration(principal: FieldsPrincipal, entityTypes: readonly string[]): Promise<{ policyVersion: string }> | { policyVersion: string }
  authorizedQueryScope?(principal: FieldsPrincipal, action: 'read', entityTypes: readonly string[]): FieldsAuthorizedQueryScopeLike
}
export interface FieldsContext {
  readonly sanitizer?: { sanitizeHtml(input: string): Promise<string> | string }
  readonly fieldTypeRegistry?: FieldTypeRegistry
  readonly cloneResolver?: { resolve(definition: AnyFieldDefinition): readonly AnyFieldDefinition[] }
  readonly principal?: FieldsPrincipal
  readonly authorization?: unknown
}
export interface AuthorizedFieldsContext extends FieldsContext {
  readonly principal: FieldsPrincipal
  readonly authorization: FieldsAuthorization
}
export function requireAuthorizedFieldsContext(context: FieldsContext): AuthorizedFieldsContext {
  const authorization=context.authorization as Partial<FieldsAuthorization> | undefined
  if (!context.principal || !authorization || typeof authorization.assert !== 'function' || typeof authorization.assertDefinitionMigration !== 'function') {
    throw new FieldValidationError('context', 'principal and authorization are required')
  }
  return context as AuthorizedFieldsContext
}

export interface ResolvedAnyFieldDefinition {
  readonly key: FieldKey
  readonly name: string
  readonly label: string
  readonly type: string
  readonly settings: unknown
  readonly required?: boolean
  readonly readOnly?: boolean
}

export interface RuntimeFieldTypeDescriptor {
  readonly type: string
  readonly contractVersion: string
  readonly schemaHash: string
  readonly codecHash: string
  parseDefinition(input: unknown, limits: DefinitionLimits): ResolvedAnyFieldDefinition
  parseValue(input: unknown, settings: unknown, limits: ValueLimits): unknown
  parseWrite(input: unknown, settings: unknown, limits: ValueLimits): unknown
  serialize(value: unknown, settings: unknown): FieldStorageValue
  deserialize(value: FieldStorageValue, settings: unknown): unknown
  format(value: unknown, settings: unknown, context: FieldsContext): Promise<FieldPresentation<unknown>> | FieldPresentation<unknown>
  schema(settings: unknown): FieldHeadlessSchema
  redactForRevision(value: unknown, settings: unknown): RevisionFieldValue | 'omit'
}

export interface CustomFieldTypeDescriptor<K extends string, S, V, W = V, F = V> extends RuntimeFieldTypeDescriptor {
  readonly type: K
  readonly contract?: { readonly settings: S; readonly value: V; readonly write: W; readonly formatted: F }
}

const customFieldDefinitionBrand: unique symbol = Symbol('custom-field-definition') as never
export interface TypedCustomFieldDefinition<K extends string = string, S = unknown, V = unknown, W = V, F = V> {
  readonly key: FieldKey
  readonly name: string
  readonly label: string
  readonly type: K
  readonly settings: S
  readonly required?: boolean
  readonly readOnly?: boolean
  readonly [customFieldDefinitionBrand]: true
  readonly customContract?: { readonly value: V; readonly write: W; readonly formatted: F }
}

export type ResolvedFieldDefinition = AnyFieldDefinition | TypedCustomFieldDefinition
export interface FieldTypeRegistry {
  readonly version: string
  readonly builtInTypes: ReadonlySet<FieldType>
  has(type: string): boolean
  resolve(type: string): RuntimeFieldTypeDescriptor | undefined
  parseDefinition(input: unknown, limits: DefinitionLimits): ResolvedAnyFieldDefinition
}

const CUSTOM_TYPE_RE = /^[a-z][a-z0-9-]*:[a-z][a-z0-9._-]*$/
const HEX64 = /^[a-f0-9]{64}$/i
export const FIELD_DEFINITION_LIMITS: DefinitionLimits = Object.freeze({ maxBytes: 256 * 1024, maxDepth: 12, maxChildren: 1_000 })
export const FIELD_VALUE_LIMITS: ValueLimits = Object.freeze({ maxBytes: 1024 * 1024, maxDepth: 12, maxNodes: 1_000 })

function fail(field: string, detail: string): never { throw new FieldValidationError(field, detail) }
function object(input: unknown, field: string): Record<string, unknown> {
  if (typeof input !== 'object' || input === null || Array.isArray(input) || Object.getPrototypeOf(input) !== Object.prototype) fail(field, 'must be a plain object')
  return input as Record<string, unknown>
}
function string(input: unknown, field: string, max = 4096): string {
  if (typeof input !== 'string' || input.length === 0 || input.length > max) fail(field, `must be a non-empty string up to ${max} characters`)
  return input
}
function json(value: unknown, limits: ValueLimits, field = 'value', depth = 0, budget = { nodes: 0, bytes: 0 }): FieldStorageValue {
  if (depth > limits.maxDepth) fail(field, `exceeds maximum depth ${limits.maxDepth}`)
  if (value === null || typeof value === 'boolean') return value
  if (typeof value === 'number') { if (!Number.isFinite(value)) fail(field, 'contains a non-finite number'); budget.bytes += 8; return value }
  if (typeof value === 'string') { budget.bytes += new TextEncoder().encode(value).byteLength; if (budget.bytes > limits.maxBytes) fail(field, `exceeds maximum size ${limits.maxBytes}`); return value }
  if (++budget.nodes > limits.maxNodes) fail(field, `exceeds maximum node count ${limits.maxNodes}`)
  if (Array.isArray(value)) return Object.freeze(value.map((item, index) => json(item, limits, `${field}.${index}`, depth + 1, budget)))
  const source = object(value, field), out: Record<string, FieldStorageValue> = {}
  for (const key of Object.keys(source).sort()) {
    if (key === '__proto__' || key === 'prototype' || key === 'constructor') fail(`${field}.${key}`, 'contains a forbidden key')
    out[key] = json(source[key], limits, `${field}.${key}`, depth + 1, budget)
  }
  return Object.freeze(out)
}

function validateDescriptor(descriptor: RuntimeFieldTypeDescriptor): void {
  if (!CUSTOM_TYPE_RE.test(descriptor.type)) fail('descriptor.type', 'extension type must use an adopter-owned namespace such as vendor:field')
  if (!descriptor.contractVersion || descriptor.contractVersion.length > 128) fail('descriptor.contractVersion', 'must be a bounded non-empty string')
  if (!HEX64.test(descriptor.schemaHash)) fail('descriptor.schemaHash', 'must be a 64-character hexadecimal digest')
  if (!HEX64.test(descriptor.codecHash)) fail('descriptor.codecHash', 'must be a 64-character hexadecimal digest')
  for (const method of ['parseDefinition','parseValue','parseWrite','serialize','deserialize','format','schema','redactForRevision'] as const) if (typeof descriptor[method] !== 'function') fail(`descriptor.${method}`, 'must be a function')
  json(descriptor.schema({}), FIELD_VALUE_LIMITS, 'descriptor.schema')
}

function descriptorHash(seed: string): string {
  let a = 0x811c9dc5, b = 0x9e3779b9
  for (let round = 0; round < 8; round++) {
    for (let i = 0; i < seed.length; i++) { a = Math.imul(a ^ (seed.charCodeAt(i) + round), 0x01000193); b = Math.imul(b ^ ((seed.charCodeAt(i) << (i % 8)) + round), 0x85ebca6b) }
    seed = `${seed}:${(a >>> 0).toString(16)}:${(b >>> 0).toString(16)}`
  }
  return `${(a>>>0).toString(16).padStart(8,'0')}${(b>>>0).toString(16).padStart(8,'0')}`.repeat(4).slice(0,64)
}
function version(descriptors: readonly RuntimeFieldTypeDescriptor[]): string {
  return `fields-registry-v1:${descriptorHash(descriptors.map((d)=>`${d.type}:${d.contractVersion}:${d.schemaHash}:${d.codecHash}`).join('|'))}`
}

export function createFieldTypeRegistry(input: { extensions?: readonly RuntimeFieldTypeDescriptor[] } = {}): FieldTypeRegistry {
  const builtIns = createBuiltinFieldTypeDescriptors()
  const builtInTypes = new Set<FieldType>(builtIns.map((descriptor) => descriptor.type as FieldType))
  const extensions = [...(input.extensions ?? [])]
  const seen = new Set<string>(builtInTypes)
  for (const descriptor of extensions) { validateDescriptor(descriptor); if (seen.has(descriptor.type)) fail('descriptor.type', `duplicate or reserved field type ${descriptor.type}`); seen.add(descriptor.type) }
  extensions.sort((left, right) => left.type.localeCompare(right.type))
  const ordered = Object.freeze([...builtIns, ...extensions.map((descriptor) => Object.freeze(descriptor))])
  const map = new Map(ordered.map((descriptor) => [descriptor.type, descriptor]))
  return Object.freeze({
    version: version(ordered), builtInTypes,
    has: (type: string) => map.has(type), resolve: (type: string) => map.get(type),
    parseDefinition(input: unknown, limits: DefinitionLimits) {
      const raw = object(input, 'definition'), type = string(raw.type, 'definition.type', 256), descriptor = map.get(type)
      if (!descriptor) fail('definition.type', `unregistered field type ${type}`)
      return descriptor.parseDefinition(input, limits)
    },
  })
}

export function defineCustomField<K extends string, S, V, W, F>(descriptor: CustomFieldTypeDescriptor<K,S,V,W,F>, definition: Omit<TypedCustomFieldDefinition<K,S,V,W,F>, typeof customFieldDefinitionBrand | 'customContract'>): TypedCustomFieldDefinition<K,S,V,W,F> {
  validateDescriptor(descriptor)
  if (definition.type !== descriptor.type) fail('definition.type', 'must match descriptor type')
  const parsed = descriptor.parseDefinition(definition, FIELD_DEFINITION_LIMITS)
  if (parsed.type !== descriptor.type || parsed.key !== definition.key) fail('definition', 'descriptor parser changed immutable type/key identity')
  return Object.freeze({ ...definition, [customFieldDefinitionBrand]: true }) as TypedCustomFieldDefinition<K,S,V,W,F>
}

export function isSanitizedFieldHtml(value: unknown): value is SanitizedFieldHtml {
  return typeof value === 'object' && value !== null && (value as Partial<SanitizedFieldHtml>)[sanitizedFieldHtmlBrand] === true && typeof (value as Partial<SanitizedFieldHtml>).html === 'string'
}
export async function sanitizeFieldHtml(input: string, context: FieldsContext): Promise<SanitizedFieldHtml> {
  if (!context.sanitizer) fail('sanitizer', 'is required for sanitized HTML output')
  const html = await context.sanitizer.sanitizeHtml(input)
  if (typeof html !== 'string') fail('sanitizer', 'must return a string')
  return Object.freeze({ html, [sanitizedFieldHtmlBrand]: true }) as SanitizedFieldHtml
}
export function assertSerializableFieldPresentation(value: unknown): FieldPresentation<unknown> {
  const p = object(value, 'presentation')
  if (p.kind === 'text') { if (typeof p.value !== 'string') fail('presentation.value', 'text presentation requires string'); return Object.freeze({kind:'text',value:p.value}) }
  if (p.kind === 'structured') return Object.freeze({kind:'structured',value:json(p.value,FIELD_VALUE_LIMITS,'presentation.value')})
  if (p.kind === 'sanitizedHtml' && isSanitizedFieldHtml(p.value)) return Object.freeze({kind:'sanitizedHtml',value:p.value})
  fail('presentation.kind', 'must be text, structured, or branded sanitizedHtml')
}
