import { sql, type SQLWrapper } from 'drizzle-orm'
import type { FieldStorageValue, FieldGroup, AnyFieldDefinition } from './schema.js'
import { parseFieldGroupDefinition } from './schema.js'
import { createFieldTypeRegistry, FIELD_DEFINITION_LIMITS, type FieldHeadlessSchema, type FieldTypeRegistry } from './types.js'
import type { FieldsReader, OptionsPageDefinition } from './options.js'
import { __optionsLifecycleInternals } from './options.js'
import type { FieldBlockDefinition } from './blocks.js'
import { __fieldBlockLifecycleInternals } from './blocks.js'
import { fieldsRequestHash, type CodeFieldsRegistrySnapshot } from './definition-lifecycle.js'

export interface FieldsHeadlessFieldSchema {
  readonly key:string
  readonly name:string
  readonly label:string
  readonly type:string
  readonly required:boolean
  readonly readOnly:boolean
  readonly value:FieldHeadlessSchema
  readonly formatted:Readonly<Record<string,FieldStorageValue>>
  readonly conditionalLogic?:unknown
}
export interface FieldsHeadlessGroupSchema {
  readonly key:string;readonly title:string;readonly version:number;readonly revision:number;readonly active:boolean;readonly origin:'code'|'db'|'import'
  readonly location:FieldGroup['location'];readonly fields:readonly FieldsHeadlessFieldSchema[]
  readonly authorization:Readonly<{readAction:'read';writeAction:'write';manageAction:'manageDefinitions'}>
}
export interface FieldsHeadlessOptionsSchema {
  readonly key:string;readonly title:string;readonly version:number;readonly revision:number;readonly active:boolean;readonly origin:'code'|'db'|'import'
  readonly parentKey?:string;readonly fieldGroupKeys:readonly string[]
  readonly authorization:Readonly<{readCapability:string;writeCapability:string;manageCapability:string}>
}
export interface FieldsHeadlessBlockSchema {
  readonly key:string;readonly title:string;readonly version:number;readonly revision:number;readonly active:boolean;readonly origin:'code'|'db'|'import'
  readonly fieldGroupKeys:readonly string[];readonly parent?:readonly string[];readonly ancestor?:readonly string[]
  readonly supports?:FieldBlockDefinition['supports'];readonly templateLock?:FieldBlockDefinition['templateLock']
  readonly tree:Readonly<Record<string,FieldStorageValue>>
}
export interface FieldsHeadlessSchema {
  readonly version:string
  readonly registryVersion:string
  readonly codeRegistryVersion?:string
  readonly codeRegistryHash?:string
  readonly groups:readonly FieldsHeadlessGroupSchema[]
  readonly optionsPages:readonly FieldsHeadlessOptionsSchema[]
  readonly blocks:readonly FieldsHeadlessBlockSchema[]
  readonly revision:Readonly<Record<string,FieldStorageValue>>
  readonly errors:readonly Readonly<{category:'validation'|'authorization'|'capability'|'conflict'|'integrity';fieldPath:boolean}>[]
}
export interface FieldsHeadlessSchemaOptions { readonly registry?:FieldTypeRegistry; readonly codeRegistry?:CodeFieldsRegistrySnapshot }
export type FieldsHeadlessErrorCode='invalid-registry'|'invalid-definition'|'integrity'
export class FieldsHeadlessError extends Error { override readonly name='FieldsHeadlessError';constructor(readonly code:FieldsHeadlessErrorCode,readonly detail:string,readonly field?:string){super(`fields headless ${code}: ${detail}${field?` (${field})`:''}`)} }
export function isFieldsHeadlessError(error:unknown):error is FieldsHeadlessError{return error instanceof FieldsHeadlessError}

interface GroupRow extends Record<string,unknown>{key:string;origin:'code'|'db'|'import';version:number|string;active:boolean|number;currentRevision:number|string;definition:unknown}
interface OptionRow extends Record<string,unknown>{key:string;origin:'code'|'db'|'import';version:number|string;active:boolean|number;currentRevision:number|string;definition:unknown}
interface BlockRow extends Record<string,unknown>{key:string;origin:'code'|'db'|'import';version:number|string;active:boolean|number;currentRevision:number|string;definition:unknown}
function rowsOf<Row extends Record<string,unknown>>(value:unknown):readonly Row[]{if(Array.isArray(value))return value as Row[];if(typeof value==='object'&&value!==null&&Array.isArray((value as {rows?:unknown}).rows))return (value as {rows:Row[]}).rows;throw new FieldsHeadlessError('integrity','database adapter returned invalid rows')}
async function execute<Row extends Record<string,unknown>>(db:FieldsReader,query:SQLWrapper):Promise<readonly Row[]>{return rowsOf<Row>(await db.execute(query))}
function parseJson(value:unknown,field:string):unknown{if(typeof value!=='string')return value;try{return JSON.parse(value) as unknown}catch{throw new FieldsHeadlessError('integrity',`${field} contains invalid JSON`)}}
function plain(value:unknown,field:string):Record<string,unknown>{const parsed=parseJson(value,field);if(typeof parsed!=='object'||parsed===null||Array.isArray(parsed)||Object.getPrototypeOf(parsed)!==Object.prototype)throw new FieldsHeadlessError('invalid-definition','must be a plain object',field);return parsed as Record<string,unknown>}
function positive(value:unknown,field:string):number{const n=Number(value);if(!Number.isSafeInteger(n)||n<1)throw new FieldsHeadlessError('integrity','must be a positive integer',field);return n}
function bool(value:unknown):boolean{return value===true||value===1}
function serializable(value:unknown,field='schema',depth=0):FieldStorageValue{if(depth>24)throw new FieldsHeadlessError('invalid-definition','schema exceeds maximum depth',field);if(value===null||typeof value==='string'||typeof value==='boolean')return value;if(typeof value==='number'){if(!Number.isFinite(value))throw new FieldsHeadlessError('invalid-definition','schema contains non-finite number',field);return value}if(Array.isArray(value))return Object.freeze(value.map((item,index)=>serializable(item,`${field}.${index}`,depth+1)));if(typeof value==='object'){const out:Record<string,FieldStorageValue>={};for(const key of Object.keys(value).sort()){const child=(value as Record<string,unknown>)[key];if(child!==undefined)out[key]=serializable(child,`${field}.${key}`,depth+1)}return Object.freeze(out)}throw new FieldsHeadlessError('invalid-definition','schema contains unsupported value',field)}

function sanitizeCustom(value:unknown,registry:FieldTypeRegistry):unknown{const row=plain(value,'field'),type=String(row.type),copy:Record<string,unknown>={...row};if(!registry.has(type))throw new FieldsHeadlessError('invalid-registry',`unregistered field type ${type}`,'field.type');if(type==='group'||type==='repeater'){const s=plain(row.settings,'field.settings'),fields=s.fields;if(!Array.isArray(fields))throw new FieldsHeadlessError('invalid-definition','nested fields are malformed','field.settings.fields');copy.settings={...s,fields:fields.map((child)=>sanitizeCustom(child,registry))}}if(type==='flexible'){const s=plain(row.settings,'field.settings'),layouts=s.layouts;if(!Array.isArray(layouts))throw new FieldsHeadlessError('invalid-definition','layouts are malformed','field.settings.layouts');copy.settings={...s,layouts:layouts.map((raw)=>{const layout=plain(raw,'layout'),fields=layout.fields;if(!Array.isArray(fields))throw new FieldsHeadlessError('invalid-definition','layout fields are malformed','layout.fields');return {...layout,fields:fields.map((child)=>sanitizeCustom(child,registry))}})}}return registry.builtInTypes.has(type as never)?copy:{...copy,type:'text',settings:{}}}
function validateFieldTree(value:unknown,registry:FieldTypeRegistry):void{const raw=plain(value,'field'),type=String(raw.type),descriptor=registry.resolve(type);if(!descriptor)throw new FieldsHeadlessError('invalid-registry',`unregistered field type ${type}`,'field.type');const parsed=descriptor.parseDefinition(registry.builtInTypes.has(type as never)?sanitizeCustom(raw,registry):raw,FIELD_DEFINITION_LIMITS);if(parsed.type!==type)throw new FieldsHeadlessError('invalid-registry','descriptor changed immutable type identity','field.type');if(type==='group'||type==='repeater')for(const child of plain(raw.settings,'field.settings').fields as unknown[])validateFieldTree(child,registry);if(type==='flexible')for(const layoutRaw of plain(raw.settings,'field.settings').layouts as unknown[])for(const child of plain(layoutRaw,'layout').fields as unknown[])validateFieldTree(child,registry)}
function parseGroup(value:unknown,registry:FieldTypeRegistry):FieldGroup{const raw=plain(value,'group'),fields=raw.fields;if(!Array.isArray(fields))throw new FieldsHeadlessError('invalid-definition','group fields are malformed','fields');for(const field of fields)validateFieldTree(field,registry);const structural=parseFieldGroupDefinition({...raw,fields:fields.map((field)=>sanitizeCustom(field,registry))});return Object.freeze({...structural,fields:Object.freeze(fields.map((field)=>Object.freeze({...plain(field,'field')})))}) as unknown as FieldGroup}

const ADMIN_TYPES=new Set(['message','accordion','tab'])
function publicChildFields(fields:readonly AnyFieldDefinition[],registry:FieldTypeRegistry):readonly FieldsHeadlessFieldSchema[]{return Object.freeze(fields.map((field)=>fieldSchema(field,registry)).filter((field):field is FieldsHeadlessFieldSchema=>field!==null))}
function structuredValueSchema(field:AnyFieldDefinition,registry:FieldTypeRegistry):FieldHeadlessSchema|null{
  if(field.type==='group'){
    const settings=field.settings as {fields:readonly AnyFieldDefinition[];layout?:'block'|'table'|'row'}
    return Object.freeze({type:'group',nullable:false,constraints:serializable({...(settings.layout?{layout:settings.layout}:{}),fields:publicChildFields(settings.fields,registry)},'field.group.constraints') as Readonly<Record<string,unknown>>})
  }
  if(field.type==='repeater'){
    const settings=field.settings as {fields:readonly AnyFieldDefinition[];layout?:'block'|'table'|'row';min?:number;max?:number;collapsedBy?:string;pagination?:{pageSize:number}}
    const publicKeys=new Set(publicChildFields(settings.fields,registry).map((child)=>child.key)),fields=publicChildFields(settings.fields,registry)
    return Object.freeze({type:'repeater',nullable:false,constraints:serializable({fields,...(settings.layout?{layout:settings.layout}:{}),...(settings.min===undefined?{}:{min:settings.min}),...(settings.max===undefined?{}:{max:settings.max}),...(settings.collapsedBy&&publicKeys.has(settings.collapsedBy)?{collapsedBy:settings.collapsedBy}:{}),...(settings.pagination?{pagination:{pageSize:settings.pagination.pageSize}}:{})},'field.repeater.constraints') as Readonly<Record<string,unknown>>})
  }
  if(field.type==='flexible'){
    const settings=field.settings as {layouts:readonly {key:string;name:string;label:string;fields:readonly AnyFieldDefinition[]}[];min?:number;max?:number}
    return Object.freeze({type:'flexible',nullable:false,constraints:serializable({layouts:settings.layouts.map((layout)=>({key:layout.key,name:layout.name,label:layout.label,fields:publicChildFields(layout.fields,registry)})),...(settings.min===undefined?{}:{min:settings.min}),...(settings.max===undefined?{}:{max:settings.max})},'field.flexible.constraints') as Readonly<Record<string,unknown>>})
  }
  if(field.type==='clone'){
    const settings=field.settings as {display?:'seamless'|'group';prefixNames?:boolean}
    return Object.freeze({type:'clone',nullable:false,constraints:serializable({resolution:'host',...(settings.display?{display:settings.display}:{}),...(settings.prefixNames===undefined?{}:{prefixNames:settings.prefixNames})},'field.clone.constraints') as Readonly<Record<string,unknown>>})
  }
  return null
}
function fieldSchema(field:AnyFieldDefinition,registry:FieldTypeRegistry):FieldsHeadlessFieldSchema|null{
  if(field.disabled===true||ADMIN_TYPES.has(field.type))return null
  let value:FieldHeadlessSchema
  if(field.type==='password')value=Object.freeze({type:'object',nullable:false,readOnly:true,constraints:Object.freeze({properties:Object.freeze({configured:Object.freeze({type:'boolean'})})})})
  else value=structuredValueSchema(field,registry)??(()=>{const descriptor=registry.resolve(field.type);if(!descriptor)throw new FieldsHeadlessError('invalid-registry',`unregistered field type ${field.type}`);return descriptor.schema(field.settings)})()
  const formatted=Object.freeze({oneOf:Object.freeze([Object.freeze({kind:'text',value:'string'}),Object.freeze({kind:'structured',value:'json'}),Object.freeze({kind:'sanitizedHtml',value:'string'})])}) as unknown as Readonly<Record<string,FieldStorageValue>>
  return Object.freeze({key:field.key,name:field.name,label:field.label,type:field.type,required:field.required===true,readOnly:field.readOnly===true||field.type==='password'||value.readOnly===true,value:Object.freeze({...value,constraints:serializable(value.constraints,'field.constraints') as Readonly<Record<string,unknown>>}),formatted,...(field.conditionalLogic?{conditionalLogic:serializable(field.conditionalLogic,'field.conditionalLogic')}: {})})
}
function publicFields(group:FieldGroup,registry:FieldTypeRegistry):readonly FieldsHeadlessFieldSchema[]{return publicChildFields(group.fields,registry)}
function blockTreeSchema():Readonly<Record<string,FieldStorageValue>>{return Object.freeze({version:1,node:Object.freeze({oneOf:Object.freeze([Object.freeze({kind:'inline',required:Object.freeze(['id','type','blockDefinitionRevision','fieldGroups']),children:'recursive'}),Object.freeze({kind:'reusable',required:Object.freeze(['id','reusableId'])})])})})}
async function assertCodeRegistry(snapshot:CodeFieldsRegistrySnapshot|undefined):Promise<void>{if(!snapshot)return;const hash=await fieldsRequestHash({version:snapshot.version,groups:snapshot.groups,optionsPages:snapshot.optionsPages,blocks:snapshot.blocks});if(hash!==snapshot.canonicalHash)throw new FieldsHeadlessError('invalid-registry','code registry hash mismatch')}

export async function generateFieldsHeadlessSchema(db:FieldsReader,options:FieldsHeadlessSchemaOptions={}):Promise<FieldsHeadlessSchema>{
  const registry=options.registry??createFieldTypeRegistry();await assertCodeRegistry(options.codeRegistry)
  const groupRows=await execute<GroupRow>(db,sql`SELECT key,origin,version,active,current_revision AS "currentRevision",definition FROM field_group_definitions WHERE active=true AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_group_definitions.host_session_id AND j.state='published')) ORDER BY key`)
  const optionRows=await execute<OptionRow>(db,sql`SELECT key,origin,version,active,current_revision AS "currentRevision",definition FROM field_options_definitions WHERE active=true AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_options_definitions.host_session_id AND j.state='published')) ORDER BY key`)
  const blockRows=await execute<BlockRow>(db,sql`SELECT key,origin,version,active,current_revision AS "currentRevision",definition FROM field_block_definitions WHERE active=true AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_block_definitions.host_session_id AND j.state='published')) ORDER BY key`)

  const groupMap=new Map<string,{definition:FieldGroup;version:number;revision:number;active:boolean;origin:'code'|'db'|'import'}>()
  for(const row of groupRows)groupMap.set(row.key,{definition:parseGroup(row.definition,registry),version:positive(row.version,'group.version'),revision:positive(row.currentRevision,'group.revision'),active:bool(row.active),origin:row.origin})
  for(const code of options.codeRegistry?.groups??[]){const {origin:_origin,...definition}=code;const parsed=parseGroup(definition,registry),previous=groupMap.get(parsed.key);groupMap.set(parsed.key,{definition:parsed,version:previous?.version??1,revision:previous?.revision??1,active:parsed.active,origin:'code'})}
  const groups:FieldsHeadlessGroupSchema[]=[...groupMap.values()].filter((item)=>item.active).sort((a,b)=>a.definition.key.localeCompare(b.definition.key)).map((item)=>Object.freeze({key:item.definition.key,title:item.definition.title,version:item.version,revision:item.revision,active:item.active,origin:item.origin,location:item.definition.location,fields:publicFields(item.definition,registry),authorization:Object.freeze({readAction:'read' as const,writeAction:'write' as const,manageAction:'manageDefinitions' as const})}))
  const groupKeys=new Set(groups.map((group)=>group.key))
  const groupDefinitions=new Map([...groupMap].map(([key,value])=>[key,value.definition]))

  const optionMap=new Map<string,{definition:OptionsPageDefinition;version:number;revision:number;active:boolean;origin:'code'|'db'|'import'}>()
  for(const row of optionRows)optionMap.set(row.key,{definition:__optionsLifecycleInternals.normalizeOptionsPage(parseJson(row.definition,'options') as never),version:positive(row.version,'options.version'),revision:positive(row.currentRevision,'options.revision'),active:bool(row.active),origin:row.origin})
  for(const code of options.codeRegistry?.optionsPages??[]){const {origin:_origin,...definition}=code,parsed=__optionsLifecycleInternals.normalizeOptionsPage(definition),previous=optionMap.get(parsed.key);optionMap.set(parsed.key,{definition:parsed,version:previous?.version??1,revision:previous?.revision??1,active:true,origin:'code'})}
  const optionsPages:FieldsHeadlessOptionsSchema[]=[...optionMap.values()].filter((item)=>item.active).sort((a,b)=>a.definition.key.localeCompare(b.definition.key)).map((item)=>{
    const page=item.definition,fieldGroupKeys=[...groupDefinitions.values()].filter((group)=>group.active&&group.location.some((rules)=>rules.some((rule)=>rule.parameter==='optionsPage'&&rule.operator==='eq'&&rule.value===page.key))).map((group)=>group.key).sort()
    return Object.freeze({key:page.key,title:page.title,version:item.version,revision:item.revision,active:item.active,origin:item.origin,...(page.parentKey?{parentKey:page.parentKey}:{}),fieldGroupKeys:Object.freeze(fieldGroupKeys),authorization:Object.freeze({readCapability:page.readCapability,writeCapability:page.writeCapability,manageCapability:page.manageCapability})})
  })

  const blockMap=new Map<string,{definition:FieldBlockDefinition;version:number;revision:number;active:boolean;origin:'code'|'db'|'import'}>()
  for(const row of blockRows)blockMap.set(row.key,{definition:__fieldBlockLifecycleInternals.normalizeDefinition(parseJson(row.definition,'block') as never),version:positive(row.version,'block.version'),revision:positive(row.currentRevision,'block.revision'),active:bool(row.active),origin:row.origin})
  for(const code of options.codeRegistry?.blocks??[]){const {origin:_origin,...definition}=code,parsed=__fieldBlockLifecycleInternals.normalizeDefinition(definition),previous=blockMap.get(parsed.key);blockMap.set(parsed.key,{definition:parsed,version:previous?.version??1,revision:previous?.revision??1,active:true,origin:'code'})}
  const blocks:FieldsHeadlessBlockSchema[]=[...blockMap.values()].filter((item)=>item.active).sort((a,b)=>a.definition.key.localeCompare(b.definition.key)).map((item)=>{const block=item.definition;for(const key of block.fieldGroupKeys)if(!groupKeys.has(key))throw new FieldsHeadlessError('integrity',`block ${block.key} references unavailable group ${key}`);return Object.freeze({key:block.key,title:block.title,version:item.version,revision:item.revision,active:item.active,origin:item.origin,fieldGroupKeys:block.fieldGroupKeys,...(block.parent?{parent:block.parent}:{}),...(block.ancestor?{ancestor:block.ancestor}:{}),...(block.supports?{supports:block.supports}:{}),...(block.templateLock===undefined?{}:{templateLock:block.templateLock}),tree:blockTreeSchema()})})
  const blockKeys=new Set(blocks.map((block)=>block.key));for(const block of blocks){for(const parent of block.parent??[])if(!blockKeys.has(parent))throw new FieldsHeadlessError('integrity',`block ${block.key} references unavailable parent ${parent}`);for(const ancestor of block.ancestor??[])if(!blockKeys.has(ancestor))throw new FieldsHeadlessError('integrity',`block ${block.key} references unavailable ancestor ${ancestor}`)}

  const revision=Object.freeze({type:'object',required:Object.freeze(['id','entityType','entityId','parentRevisionId','definitionVersions','values','createdAt','createdBy','sourceVersion']),properties:Object.freeze({definitionVersions:'record<string,positive-integer>',values:'redacted-field-value-map',blockDocument:'block-tree',password:'configured-marker-only'})})
  const errors=Object.freeze((['validation','authorization','capability','conflict','integrity'] as const).map((category)=>Object.freeze({category,fieldPath:true})))
  const material={registryVersion:registry.version,...(options.codeRegistry?{codeRegistryVersion:options.codeRegistry.version,codeRegistryHash:options.codeRegistry.canonicalHash}:{}),groups,optionsPages,blocks,revision,errors}
  return Object.freeze({version:await fieldsRequestHash(material),...material})
}
