import { FieldValidationError } from './errors.js'
import type { AnyFieldDefinition, ConditionalRule, ConditionalRuleGroups, FieldPath } from './schema.js'

function fail(field:string,detail:string):never{throw new FieldValidationError(field,detail)}
function isEmpty(value:unknown):boolean{return value===null||value===undefined||value===''||(Array.isArray(value)&&value.length===0)}
function equal(left:unknown,right:unknown):boolean{return typeof left===typeof right&&left===right}

export function resolveConditionalPath(values:unknown,path:FieldPath):readonly unknown[]{
  let current:unknown[]=[values]
  for(const segment of path){const next:unknown[]=[];for(const value of current){
    if(Array.isArray(value)){if(typeof segment==='number'){if(segment>=0&&segment<value.length)next.push(value[segment])}else for(const row of value){if(typeof row==='object'&&row!==null){const rec=row as Record<string,unknown>,source=typeof rec.values==='object'&&rec.values!==null?rec.values as Record<string,unknown>:rec;if(Object.prototype.hasOwnProperty.call(source,segment))next.push(source[segment])}}}
    else if(typeof value==='object'&&value!==null){const rec=value as Record<string,unknown>;if(typeof segment==='string'&&Object.prototype.hasOwnProperty.call(rec,segment))next.push(rec[segment]);else if(typeof segment==='number'&&Array.isArray(rec.values)&&segment<rec.values.length)next.push(rec.values[segment])}
  }current=next;if(current.length===0)break}return Object.freeze(current)
}
function scalarRule(rule:ConditionalRule,value:unknown,missing:boolean):boolean{
  if(rule.operator==='empty')return missing||isEmpty(value);if(rule.operator==='notEmpty')return !missing&&!isEmpty(value);if(missing)return rule.operator==='neq'||rule.operator==='notContains'
  if(rule.operator==='eq'||rule.operator==='neq'){const result=equal(value,rule.value);return rule.operator==='eq'?result:!result}
  if(rule.operator==='contains'||rule.operator==='notContains'){let result=false;if(typeof value==='string'&&typeof rule.value==='string')result=value.includes(rule.value);else if(Array.isArray(value))result=value.some((item)=>equal(item,rule.value));return rule.operator==='contains'?result:!result}
  if(rule.operator==='gt'||rule.operator==='gte'||rule.operator==='lt'||rule.operator==='lte'){if(typeof value!=='number'||!Number.isFinite(value))return false;if(rule.operator==='gt')return value>rule.value;if(rule.operator==='gte')return value>=rule.value;if(rule.operator==='lt')return value<rule.value;return value<=rule.value}
  if(rule.operator==='matches'){if(typeof value!=='string')return false;if(rule.value.source.length>512)fail('conditional.matches','pattern exceeds 512 characters');if(/\([^)]*[+*][^)]*\)[+*{]/.test(rule.value.source))fail('conditional.matches','nested unbounded quantifiers are not allowed');let rx:RegExp;try{rx=new RegExp(rule.value.source,rule.value.flags??'')}catch{fail('conditional.matches','pattern is invalid')}return rx.test(value)}
  return false
}
export function evaluateConditionalRule(rule:ConditionalRule,values:unknown):boolean{const resolved=resolveConditionalPath(values,rule.fieldPath);if(resolved.length===0)return scalarRule(rule,undefined,true);const results=resolved.map((value)=>scalarRule(rule,value,false));return rule.quantifier==='all'?results.length>0&&results.every(Boolean):results.some(Boolean)}
export function evaluateConditionalLogic(groups:ConditionalRuleGroups|undefined,values:unknown):boolean{return groups===undefined||groups.length===0||groups.some((group)=>group.length>0&&group.every((rule)=>evaluateConditionalRule(rule,values)))}

function operatorAllowed(type:string,operator:ConditionalRule['operator']):boolean{if(['empty','notEmpty','eq','neq'].includes(operator))return !['message','accordion','tab'].includes(type);if(['gt','gte','lt','lte'].includes(operator))return type==='number'||type==='range';if(operator==='matches')return ['text','textarea','email','url','richText'].includes(type);if(operator==='contains'||operator==='notContains')return ['text','textarea','email','url','checkbox','select','relationship','gallery','taxonomy','user'].includes(type);return false}
export function validateConditionalDependencies(fields:readonly AnyFieldDefinition[]):void{
  const top=new Map(fields.map((field)=>[field.key,field])),edges=new Map<string,Set<string>>()
  const visit=(field:AnyFieldDefinition,owner:string)=>{for(const group of field.conditionalLogic??[])for(const rule of group){const first=rule.fieldPath[0];if(typeof first!=='string')fail(`${owner}.conditionalLogic`,'dependency path must start with a field key');const target=top.get(first);if(!target)fail(`${owner}.conditionalLogic`,`references unknown field ${first}`);if(!operatorAllowed(target.type,rule.operator))fail(`${owner}.conditionalLogic`,`operator ${rule.operator} is not allowed for ${target.type}`);const deps=edges.get(owner)??new Set<string>();deps.add(first);edges.set(owner,deps)};if(field.type==='group'||field.type==='repeater')for(const child of (field.settings as {fields:readonly AnyFieldDefinition[]}).fields)visit(child,owner);if(field.type==='flexible')for(const layout of (field.settings as {layouts:readonly {fields:readonly AnyFieldDefinition[]}[]}).layouts)for(const child of layout.fields)visit(child,owner)}
  for(const field of fields)visit(field,field.key)
  const active=new Set<string>(),done=new Set<string>();const dfs=(key:string)=>{if(active.has(key))fail(`${key}.conditionalLogic`,'contains a dependency cycle');if(done.has(key))return;active.add(key);for(const dep of edges.get(key)??[])dfs(dep);active.delete(key);done.add(key)};for(const field of fields)dfs(field.key)
}
