import { FieldValidationError } from './errors.js'
import type { LocationRule, LocationRuleGroups, LocationValue } from './schema.js'

export interface LocationContext {
  entityType: string
  subType?: string
  entityId?: string
  status?: string
  template?: string
  parentId?: string
  taxonomy?: Readonly<Record<string, readonly string[]>>
  format?: string
  authorId?: string
  actorRoles?: readonly string[]
  surface?: 'entry' | 'term' | 'user' | 'comment' | 'media' | 'menu' | 'widget' | 'options' | 'block'
  blockType?: string
  route?: string
  optionsPage?: string
}
export interface LocationParameterResolver { readonly parameter: string; resolve(context: LocationContext): Promise<LocationValue|undefined>|LocationValue|undefined }
export interface LocationResolverRegistry { readonly version:string; evaluate(rules:LocationRuleGroups,context:LocationContext):Promise<boolean> }

function fail(field:string,detail:string):never{throw new FieldValidationError(field,detail)}
function hash(seed:string):string{let h=2166136261;for(let i=0;i<seed.length;i++)h=Math.imul(h^seed.charCodeAt(i),16777619);return (h>>>0).toString(16).padStart(8,'0')}
function scalarEqual(left:unknown,right:unknown):boolean{return typeof left===typeof right&&left===right}
function arrayValue(value:LocationValue):readonly (string|number)[]{return Array.isArray(value)?value:[]}
function match(rule:LocationRule,actual:LocationValue|undefined):boolean{
  if(actual===undefined)return rule.operator==='neq'||rule.operator==='notIn'
  if(rule.operator==='eq')return !Array.isArray(actual)&&!Array.isArray(rule.value)&&scalarEqual(actual,rule.value)
  if(rule.operator==='neq')return !match({...rule,operator:'eq'},actual)
  if(rule.operator==='in'){
    const allowed=arrayValue(rule.value)
    if(Array.isArray(actual))return actual.some((item)=>allowed.some((allowedItem)=>scalarEqual(item,allowedItem)))
    return allowed.some((item)=>scalarEqual(actual,item))
  }
  if(rule.operator==='notIn')return !match({...rule,operator:'in'},actual)
  if(rule.operator==='contains'){
    if(Array.isArray(actual))return actual.some((item)=>scalarEqual(item,rule.value))
    if(typeof actual==='string'&&typeof rule.value==='string')return actual.includes(rule.value)
    return false
  }
  return false
}

const direct=(parameter:keyof LocationContext):LocationParameterResolver=>Object.freeze({parameter:String(parameter),resolve:(context:LocationContext)=>{
  const value=context[parameter]
  if(value===undefined||typeof value==='object')return undefined
  return value as LocationValue
}})
export const BUILTIN_LOCATION_RESOLVERS:readonly LocationParameterResolver[]=Object.freeze([
  direct('entityType'),direct('subType'),direct('entityId'),direct('status'),direct('template'),direct('parentId'),direct('format'),direct('authorId'),direct('surface'),direct('blockType'),direct('route'),direct('optionsPage'),
  Object.freeze({parameter:'actorRole',resolve:(context:LocationContext)=>context.actorRoles}),
  Object.freeze({parameter:'taxonomy',resolve:(context:LocationContext)=>context.taxonomy?Object.values(context.taxonomy).flat():undefined}),
])

export function createLocationResolverRegistry(input:{builtIns?:readonly LocationParameterResolver[];extensions?:readonly LocationParameterResolver[]}={}):LocationResolverRegistry{
  const ordered=[...(input.builtIns??BUILTIN_LOCATION_RESOLVERS),...(input.extensions??[])]
  const map=new Map<string,LocationParameterResolver>()
  for(const resolver of ordered){if(!resolver.parameter||resolver.parameter.length>128)fail('location.parameter','must be a bounded non-empty string');if(map.has(resolver.parameter))fail('location.parameter',`duplicate resolver ${resolver.parameter}`);if(typeof resolver.resolve!=='function')fail('location.resolver','must be a function');map.set(resolver.parameter,Object.freeze(resolver))}
  const taxonomyPrefix='taxonomy:'
  const version=`location-v1:${hash([...map.keys()].sort().join('|'))}`
  return Object.freeze({version,async evaluate(rules:LocationRuleGroups,context:LocationContext):Promise<boolean>{
    if(!Array.isArray(rules)||rules.length===0)return false
    for(let gi=0;gi<rules.length;gi++){
      const group=rules[gi]!
      if(!Array.isArray(group)||group.length===0)fail(`location.${gi}`,'AND group must not be empty')
      let groupMatch=true
      for(let ri=0;ri<group.length;ri++){
        const rule=group[ri]!
        let resolver=map.get(rule.parameter),actual:LocationValue|undefined
        if(!resolver&&rule.parameter.startsWith(taxonomyPrefix)){
          const taxonomy=rule.parameter.slice(taxonomyPrefix.length)
          if(!taxonomy)fail(`location.${gi}.${ri}.parameter`,'taxonomy parameter must name a taxonomy')
          actual=context.taxonomy?.[taxonomy]
        }else{
          if(!resolver)fail(`location.${gi}.${ri}.parameter`,`unknown location parameter ${rule.parameter}`)
          try{actual=await resolver.resolve(context)}catch{fail(`location.${gi}.${ri}.parameter`,`resolver ${rule.parameter} failed`)}
        }
        if(!match(rule,actual)){groupMatch=false;break}
      }
      if(groupMatch)return true
    }
    return false
  }})
}
