import { sql, type SQLWrapper } from 'drizzle-orm'
import type { EntityRef } from './model.js'
import {
  FIELD_DEFINITION_LIMITS,
  createFieldTypeRegistry,
  requireAuthorizedFieldsContext,
  type FieldTypeRegistry,
  type FieldsContext,
  type ResolvedAnyFieldDefinition,
} from './types.js'
import {
  parseFieldGroupDefinition,
  type AnyFieldDefinition,
  type FieldGroup,
  type FieldPath,
  type FieldStorageValue,
  type FieldValueNodeRow,
} from './schema.js'
import { decodeFieldValueNodes, redactFieldValueForRevision } from './values.js'
import { assertFieldsAuthorized, fieldsRequestHash } from './definition-lifecycle.js'
import { __optionsLifecycleInternals, type FieldsReader, type OptionsPageDefinition } from './options.js'
import { __fieldBlockLifecycleInternals, parseBlockDocument, DEFAULT_BLOCK_POLICY, type BlockDocument, type FieldBlockDefinition } from './blocks.js'
import type { FieldsExportManifest, FieldsRevisionExport, FieldDefinitionVersionExport, FieldBlockDefinitionVersionExport } from './import.js'

export interface FieldsExportSelection {
  readonly groups?: readonly string[]
  readonly entities?: readonly EntityRef[]
  readonly includeRevisions?: boolean
  readonly includeOptions?: boolean
  readonly includeBlocks?: boolean
}

interface NodeRow extends Record<string, unknown> {
  nodeId:string; entityType:string; entityId:string; groupKey:string; definitionRevision:number|string; fieldKey:string; path:string
  parentNodeId:string|null; rowId:string|null; layoutKey:string|null; nodeKind:FieldValueNodeRow['nodeKind']; ordinal:number|string
  isNull:boolean|number; valueText:string|null; valueNumber:number|string|null; valueBoolean:boolean|number|null; valueDateTime:string|null; valueRef:string|null; valueJson:string|null
}
interface GroupVersionRow extends Record<string,unknown>{groupKey:string;revision:number|string;canonicalHash:string;definition:unknown;createdAt:Date|string;origin:'code'|'db'|'import'}
interface GroupCurrentRow extends Record<string,unknown>{key:string;definition:unknown;currentRevision:number|string}
interface RevisionRow extends Record<string,unknown>{id:string;entityType:string;entityId:string;parentRevisionId:string;definitionVersions:unknown;values:unknown;blockDocument:unknown|null;createdAt:Date|string;createdBy:string;sourceVersion:string|number}
interface OptionCurrentRow extends Record<string,unknown>{key:string;definition:unknown}
interface BlockCurrentRow extends Record<string,unknown>{key:string;definition:unknown}
interface BlockVersionRow extends Record<string,unknown>{blockKey:string;revision:number|string;canonicalHash:string;definition:unknown;createdAt:Date|string;origin:'code'|'db'|'import'}
interface BlockDocumentRow extends Record<string,unknown>{entityType:string;entityId:string;version:number|string;document:unknown;definitionVersions: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 Error('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 Error(`${field} contains invalid JSON`)}}
function record(value:unknown,field:string):Record<string,unknown>{const parsed=parseJson(value,field);if(typeof parsed!=='object'||parsed===null||Array.isArray(parsed))throw new Error(`${field} is malformed`);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 Error(`${field} is invalid`);return n}
function iso(value:unknown,field:string):string{const date=value instanceof Date?new Date(value):new Date(String(value));if(Number.isNaN(date.getTime()))throw new Error(`${field} is invalid`);return date.toISOString()}
function normalizedGroupKey(value:string):string{if(!/^[a-z][a-z0-9_]*$/.test(value))throw new Error('selection contains invalid group key');return value}
function normalizedRef(value:EntityRef):EntityRef{if(!value.entityType||!value.entityId||value.entityType.length>256||value.entityId.length>512)throw new Error('selection contains invalid entity reference');return Object.freeze({entityType:value.entityType,entityId:value.entityId})}
function node(row:NodeRow):FieldValueNodeRow{return Object.freeze({nodeId:String(row.nodeId),entityType:String(row.entityType),entityId:String(row.entityId),groupKey:String(row.groupKey),definitionRevision:Number(row.definitionRevision),fieldKey:String(row.fieldKey),path:String(row.path),parentNodeId:row.parentNodeId===null?null:String(row.parentNodeId),rowId:row.rowId===null?null:String(row.rowId),layoutKey:row.layoutKey===null?null:String(row.layoutKey),nodeKind:row.nodeKind,ordinal:Number(row.ordinal),isNull:row.isNull===true||row.isNull===1,valueText:row.valueText,valueNumber:row.valueNumber===null?null:Number(row.valueNumber),valueBoolean:row.valueBoolean===null?null:row.valueBoolean===true||row.valueBoolean===1,valueDateTime:row.valueDateTime,valueRef:row.valueRef,valueJson:row.valueJson})}

function plain(value:unknown,field:string):Record<string,unknown>{if(typeof value!=='object'||value===null||Array.isArray(value)||Object.getPrototypeOf(value)!==Object.prototype)throw new Error(`${field} must be a plain object`);return value as Record<string,unknown>}
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 Error(`unregistered field type ${type}`)
  if(type==='group'||type==='repeater'){const settings=plain(row.settings,'field.settings');copy.settings={...settings,fields:(settings.fields as unknown[]).map((child)=>sanitizeCustom(child,registry))}}
  if(type==='flexible'){const settings=plain(row.settings,'field.settings');copy.settings={...settings,layouts:(settings.layouts as unknown[]).map((layoutRaw)=>{const layout=plain(layoutRaw,'layout');return {...layout,fields:(layout.fields as unknown[]).map((child)=>sanitizeCustom(child,registry))}})}}
  if(!registry.builtInTypes.has(type as never))return {...copy,type:'text',settings:{}}
  return copy
}
function validateFieldTree(value:unknown,registry:FieldTypeRegistry):ResolvedAnyFieldDefinition{
  const raw=plain(value,'field'),type=String(raw.type),descriptor=registry.resolve(type);if(!descriptor)throw new Error(`unregistered field type ${type}`)
  const parsed=descriptor.parseDefinition(registry.builtInTypes.has(type as never)?sanitizeCustom(raw,registry):raw,FIELD_DEFINITION_LIMITS);if(parsed.type!==type)throw new Error('field descriptor changed type identity')
  if(type==='group'||type==='repeater'){const settings=plain(raw.settings,'field.settings');for(const child of settings.fields as unknown[])validateFieldTree(child,registry)}
  if(type==='flexible'){const settings=plain(raw.settings,'field.settings');for(const layoutRaw of settings.layouts as unknown[]){const layout=plain(layoutRaw,'layout');for(const child of layout.fields as unknown[])validateFieldTree(child,registry)}}
  return parsed
}
function parseGroup(value:unknown,registry:FieldTypeRegistry):FieldGroup{
  const raw=plain(parseJson(value,'field group'),'field group'),fields=raw.fields;if(!Array.isArray(fields))throw new Error('field group fields are malformed');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
}
function definitionVersions(value:unknown):Readonly<Record<string,number>>{const source=record(value,'definitionVersions'),out:Record<string,number>={};for(const [key,raw] of Object.entries(source))out[key]=positive(raw,`definitionVersions.${key}`);return Object.freeze(out)}

function redactedGroupValues(group:FieldGroup,input:Readonly<Record<string,unknown>>,registry:FieldTypeRegistry,sourceRef:EntityRef,base:FieldPath,omissions:{sourceRef:EntityRef;fieldPath:FieldPath;reason:'sensitive'|'unauthorized'}[]):Readonly<Record<string,unknown>>{
  const output:Record<string,unknown>={}
  const walk=(fields:readonly AnyFieldDefinition[],source:Readonly<Record<string,unknown>>,target:Record<string,unknown>,path:FieldPath):void=>{
    for(const field of fields){if(!Object.prototype.hasOwnProperty.call(source,field.key))continue;const value=source[field.key],fieldPath=Object.freeze([...path,field.key]) as FieldPath
      if(field.type==='password'){omissions.push(Object.freeze({sourceRef,fieldPath,reason:'sensitive'}));continue}
      if(field.type==='group'&&value&&typeof value==='object'&&!Array.isArray(value)){const child:Record<string,unknown>={};walk((field.settings as {fields:readonly AnyFieldDefinition[]}).fields,value as Record<string,unknown>,child,fieldPath);target[field.key]=Object.freeze(child);continue}
      if(field.type==='repeater'&&Array.isArray(value)){target[field.key]=Object.freeze(value.map((raw,index)=>{const row=plain(raw,`repeater.${index}`),values=plain(row.values,`repeater.${index}.values`),child:Record<string,unknown>={};walk((field.settings as {fields:readonly AnyFieldDefinition[]}).fields,values,child,Object.freeze([...fieldPath,index]) as FieldPath);return Object.freeze({...row,values:Object.freeze(child)})}));continue}
      if(field.type==='flexible'&&Array.isArray(value)){target[field.key]=Object.freeze(value.map((raw,index)=>{const row=plain(raw,`flexible.${index}`),layout=(field.settings as {layouts:readonly {key:string;fields:readonly AnyFieldDefinition[]}[]}).layouts.find((item)=>item.key===row.layoutKey);if(!layout)return row;const values=plain(row.values,`flexible.${index}.values`),child:Record<string,unknown>={};walk(layout.fields,values,child,Object.freeze([...fieldPath,index]) as FieldPath);return Object.freeze({...row,values:Object.freeze(child)})}));continue}
      if(field.type==='clone'){omissions.push(Object.freeze({sourceRef,fieldPath,reason:'sensitive'}));continue}
      const descriptor=registry.resolve(field.type);if(!descriptor)throw new Error(`unregistered field type ${field.type}`);const redacted=redactFieldValueForRevision(field as never,value,registry);if(redacted==='omit'){omissions.push(Object.freeze({sourceRef,fieldPath,reason:'sensitive'}));continue}target[field.key]=redacted
    }
  }
  walk(group.fields,input,output,base)
  return Object.freeze(output)
}
async function readGroupVersion(db:FieldsReader,groupKey:string,revision:number,registry:FieldTypeRegistry):Promise<{definition:FieldGroup;row:GroupVersionRow}>{const rows=await execute<GroupVersionRow>(db,sql`SELECT group_key AS "groupKey",revision,canonical_hash AS "canonicalHash",definition,created_at AS "createdAt",origin FROM field_definition_versions WHERE group_key=${groupKey} AND revision=${revision} AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_definition_versions.host_session_id AND j.state='published')) LIMIT 1`),row=rows[0];if(!row)throw new Error(`immutable field definition missing: ${groupKey}@${revision}`);const definition=parseGroup(row.definition,registry);return {definition,row}}
async function readEntityValues(db:FieldsReader,refValue:EntityRef,registry:FieldTypeRegistry,omissions:{sourceRef:EntityRef;fieldPath:FieldPath;reason:'sensitive'|'unauthorized'}[]):Promise<{values:Readonly<Record<string,Readonly<Record<string,unknown>>>>;definitionVersions:Readonly<Record<string,number>>}>{
  const rows=await execute<NodeRow>(db,sql`SELECT node_id AS "nodeId",entity_type AS "entityType",entity_id AS "entityId",group_key AS "groupKey",definition_revision AS "definitionRevision",field_key AS "fieldKey",path,parent_node_id AS "parentNodeId",row_id AS "rowId",layout_key AS "layoutKey",node_kind AS "nodeKind",ordinal,is_null AS "isNull",value_text AS "valueText",value_number AS "valueNumber",value_boolean AS "valueBoolean",value_date_time AS "valueDateTime",value_ref AS "valueRef",value_json AS "valueJson" FROM field_value_nodes WHERE entity_type=${refValue.entityType} AND entity_id=${refValue.entityId} AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_value_nodes.host_session_id AND j.state='published')) ORDER BY group_key,path`)
  const buckets=new Map<string,NodeRow[]>();for(const row of rows){const bucket=buckets.get(row.groupKey)??[];bucket.push(row);buckets.set(row.groupKey,bucket)}
  const values:Record<string,Readonly<Record<string,unknown>>>={},versions:Record<string,number>={}
  for(const [groupKey,bucket] of [...buckets].sort(([a],[b])=>a.localeCompare(b))){const revisions=[...new Set(bucket.map((row)=>Number(row.definitionRevision)))];if(revisions.length!==1)throw new Error(`entity group mixes immutable revisions: ${groupKey}`);const revision=revisions[0]!,resolved=await readGroupVersion(db,groupKey,revision,registry),decoded=decodeFieldValueNodes(resolved.definition,bucket.map(node),{definitionRevision:revision});values[groupKey]=redactedGroupValues(resolved.definition,decoded,registry,refValue,Object.freeze([groupKey]) as FieldPath,omissions);versions[groupKey]=revision}
  return {values:Object.freeze(values),definitionVersions:Object.freeze(versions)}
}

export async function exportFields(db:FieldsReader,selection:FieldsExportSelection,context:FieldsContext):Promise<FieldsExportManifest>{
  const authorized=requireAuthorizedFieldsContext(context),registry=context.fieldTypeRegistry??createFieldTypeRegistry(),omissions:{sourceRef:EntityRef;fieldPath:FieldPath;reason:'sensitive'|'unauthorized'}[]=[]
  const requestedGroups=selection.groups===undefined?null:Object.freeze([...new Set(selection.groups.map(normalizedGroupKey))].sort())
  if(requestedGroups)for(const key of requestedGroups)await assertFieldsAuthorized(context,'manageDefinitions',{entityType:'fields-group-definition',entityId:key});else await assertFieldsAuthorized(context,'manageDefinitions',{entityType:'fields-group-definition',entityId:'*'})
  const entities=Object.freeze((selection.entities??[]).map(normalizedRef));if(entities.length>10000)throw new Error('too many export entities')
  const readable:EntityRef[]=[]
  for(const entity of entities){try{await assertFieldsAuthorized(context,'read',entity);readable.push(entity)}catch{omissions.push(Object.freeze({sourceRef:entity,fieldPath:Object.freeze(['*']) as FieldPath,reason:'unauthorized'}))}}
  if(selection.includeOptions)await assertFieldsAuthorized(context,'manageDefinitions',{entityType:'fields-options-definition',entityId:'*'})
  if(selection.includeBlocks)await assertFieldsAuthorized(context,'manageDefinitions',{entityType:'fields-block-definition',entityId:'*'})

  const groupClause=requestedGroups&&requestedGroups.length?sql`AND key IN (${sql.join(requestedGroups.map((key)=>sql`${key}`),sql`, `)})`:requestedGroups?sql`AND FALSE`:sql``
  const currentRows=await execute<GroupCurrentRow>(db,sql`SELECT key,definition,current_revision AS "currentRevision" FROM field_group_definitions WHERE (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')) ${groupClause} ORDER BY key`)
  const groups=currentRows.map((row)=>parseGroup(row.definition,registry)),keys=groups.map((group)=>group.key)
  const versions:FieldDefinitionVersionExport[]=[]
  if(keys.length){const versionRows=await execute<GroupVersionRow>(db,sql`SELECT group_key AS "groupKey",revision,canonical_hash AS "canonicalHash",definition,created_at AS "createdAt",origin FROM field_definition_versions WHERE group_key IN (${sql.join(keys.map((key)=>sql`${key}`),sql`, `)}) AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_definition_versions.host_session_id AND j.state='published')) ORDER BY group_key,revision`);for(const row of versionRows)versions.push(Object.freeze({groupKey:row.groupKey,revision:positive(row.revision,'revision'),canonicalHash:String(row.canonicalHash),definition:parseGroup(row.definition,registry),createdAt:iso(row.createdAt,'createdAt'),origin:row.origin}))}

  const optionPages:OptionsPageDefinition[]=[]
  if(selection.includeOptions){const rows=await execute<OptionCurrentRow>(db,sql`SELECT key,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`);for(const row of rows)optionPages.push(__optionsLifecycleInternals.normalizeOptionsPage(parseJson(row.definition,'options definition') as never))}
  const blocks:FieldBlockDefinition[]=[],blockVersions:FieldBlockDefinitionVersionExport[]=[]
  if(selection.includeBlocks){const current=await execute<BlockCurrentRow>(db,sql`SELECT key,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`);for(const row of current)blocks.push(__fieldBlockLifecycleInternals.normalizeDefinition(parseJson(row.definition,'block definition') as never));const rows=await execute<BlockVersionRow>(db,sql`SELECT block_key AS "blockKey",revision,canonical_hash AS "canonicalHash",definition,created_at AS "createdAt",origin FROM field_block_definition_versions WHERE (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_block_definition_versions.host_session_id AND j.state='published')) ORDER BY block_key,revision`);for(const row of rows)blockVersions.push(Object.freeze({blockKey:row.blockKey,revision:positive(row.revision,'block revision'),canonicalHash:String(row.canonicalHash),definition:__fieldBlockLifecycleInternals.normalizeDefinition(parseJson(row.definition,'block version') as never),createdAt:iso(row.createdAt,'block createdAt'),origin:row.origin}))}

  const entityExports:NonNullable<FieldsExportManifest['entities']>[number][]=[]
  for(const entity of readable){const snapshot=await readEntityValues(db,entity,registry,omissions);entityExports.push(Object.freeze({sourceRef:entity,values:snapshot.values,definitionVersions:snapshot.definitionVersions}))}

  const revisions:FieldsRevisionExport[]=[]
  if(selection.includeRevisions&&readable.length){for(const entity of readable){const rows=await execute<RevisionRow>(db,sql`SELECT id,entity_type AS "entityType",entity_id AS "entityId",parent_revision_id AS "parentRevisionId",definition_versions AS "definitionVersions",values,block_document AS "blockDocument",created_at AS "createdAt",created_by AS "createdBy",source_version AS "sourceVersion" FROM field_revisions WHERE entity_type=${entity.entityType} AND entity_id=${entity.entityId} AND kind='revision' AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_revisions.host_session_id AND j.state='published')) ORDER BY created_at,id`);for(const row of rows){const pinned=definitionVersions(row.definitionVersions),sourceValues=record(row.values,'revision values'),clean:Record<string,Readonly<Record<string,unknown>>>={};for(const [groupKey,value] of Object.entries(sourceValues)){const revision=pinned[groupKey];if(!revision)continue;const resolved=await readGroupVersion(db,groupKey,revision,registry);clean[groupKey]=redactedGroupValues(resolved.definition,record(value,`revision.${groupKey}`),registry,entity,Object.freeze([groupKey]) as FieldPath,omissions)}const block=row.blockDocument===null?undefined:parseBlockDocument(JSON.stringify(parseJson(row.blockDocument,'revision block')),DEFAULT_BLOCK_POLICY);revisions.push(Object.freeze({sourceRevisionKey:row.id,sourceRef:entity,parentRevisionKey:row.parentRevisionId,definitionVersions:pinned,values:Object.freeze(clean),...(block?{blockDocument:block}:{}),createdAt:iso(row.createdAt,'revision createdAt'),createdBySourceId:row.createdBy,sourceVersion:positive(row.sourceVersion,'revision sourceVersion')}))}}}

  const options:NonNullable<FieldsExportManifest['options']>[number][]=[]
  if(selection.includeOptions){for(const page of optionPages){try{await assertFieldsAuthorized(context,'read',{entityType:'options',entityId:page.key})}catch{omissions.push(Object.freeze({sourceRef:Object.freeze({entityType:'options',entityId:page.key}),fieldPath:Object.freeze(['*']) as FieldPath,reason:'unauthorized'}));continue}const snapshot=await readEntityValues(db,{entityType:'options',entityId:page.key},registry,omissions);options.push(Object.freeze({pageKey:page.key,values:snapshot.values,definitionVersions:snapshot.definitionVersions,revision:1}))}}

  const blockDocuments:NonNullable<FieldsExportManifest['blockDocuments']>[number][]=[]
  if(selection.includeBlocks){for(const entity of readable){const rows=await execute<BlockDocumentRow>(db,sql`SELECT entity_type AS "entityType",entity_id AS "entityId",version,document,definition_versions AS "definitionVersions" FROM field_block_documents WHERE entity_type=${entity.entityType} AND entity_id=${entity.entityId} AND (host_session_id IS NULL OR EXISTS (SELECT 1 FROM fields_import_journal j WHERE j.host_session_id=field_block_documents.host_session_id AND j.state='published')) LIMIT 1`),row=rows[0];if(row)blockDocuments.push(Object.freeze({sourceRef:entity,document:parseBlockDocument(JSON.stringify(parseJson(row.document,'block document')),DEFAULT_BLOCK_POLICY),version:String(row.version),definitionVersions:definitionVersions(row.definitionVersions)}))}}

  void authorized
  return Object.freeze({version:1,definitions:Object.freeze({groups:Object.freeze(groups),versions:Object.freeze(versions),optionsPages:Object.freeze(optionPages),blocks:Object.freeze(blocks),blockVersions:Object.freeze(blockVersions)}),...(entityExports.length?{entities:Object.freeze(entityExports)}:{}),...(revisions.length?{revisions:Object.freeze(revisions)}:{}),...(options.length?{options:Object.freeze(options)}:{}),...(blockDocuments.length?{blockDocuments:Object.freeze(blockDocuments)}:{}),omissions:Object.freeze(omissions)})
}

export async function fieldsExportManifestHash(manifest:FieldsExportManifest):Promise<string>{return fieldsRequestHash(manifest)}
