import re
from collections import deque
from datetime import datetime, timedelta, timezone

NODE_TYPES={'start','end','user_task','service_task','decision','parallel_split','parallel_join','timer','message','subprocess'}
FIELD_TYPES={'text','textarea','number','currency','date','datetime','select','multiselect','checkbox','radio','email','phone','file','hidden'}
OPERATORS={'eq','ne','gt','gte','lt','lte','in','not_in','contains','starts_with','ends_with','is_empty','not_empty'}

def safe_code(value:str)->str:
    code=re.sub(r'[^a-z0-9_]+','_',value.strip().lower()).strip('_')
    if not code or len(code)>100: raise ValueError('Código inválido')
    return code

def validate_form_schema(fields:list)->dict:
    if not fields: raise ValueError('O formulário deve possuir ao menos um campo')
    names=set()
    for f in fields:
        name=safe_code(str(f.get('name','')))
        if name in names: raise ValueError(f'Campo duplicado: {name}')
        if f.get('type','text') not in FIELD_TYPES: raise ValueError(f'Tipo de campo inválido: {name}')
        names.add(name)
    return {'valid':True,'field_count':len(fields),'fields':sorted(names)}

def validate_process(nodes:list,edges:list)->dict:
    if not nodes: raise ValueError('O processo deve possuir nós')
    ids=[str(n.get('id','')) for n in nodes]
    if any(not x for x in ids) or len(ids)!=len(set(ids)): raise ValueError('IDs de nós ausentes ou duplicados')
    by_id={str(n['id']):n for n in nodes}
    for n in nodes:
        if n.get('type') not in NODE_TYPES: raise ValueError(f"Tipo de nó inválido: {n.get('type')}")
    starts=[n for n in nodes if n.get('type')=='start']; ends=[n for n in nodes if n.get('type')=='end']
    if len(starts)!=1: raise ValueError('O processo deve possuir exatamente um início')
    if not ends: raise ValueError('O processo deve possuir ao menos um fim')
    graph={x:[] for x in ids}; indegree={x:0 for x in ids}
    for e in edges:
        source=str(e.get('source','')); target=str(e.get('target',''))
        if source not in by_id or target not in by_id: raise ValueError('Conexão aponta para nó inexistente')
        if source==target: raise ValueError('Auto conexão não permitida')
        graph[source].append(target); indegree[target]+=1
    reachable=set(); q=deque([str(starts[0]['id'])])
    while q:
        x=q.popleft()
        if x in reachable: continue
        reachable.add(x); q.extend(graph[x])
    unreachable=set(ids)-reachable
    if unreachable: raise ValueError('Existem nós desconectados do início')
    return {'valid':True,'node_count':len(nodes),'edge_count':len(edges),'start_node':starts[0]['id'],'end_nodes':[n['id'] for n in ends]}

def evaluate_conditions(conditions:list,variables:dict)->bool:
    for item in sorted(conditions,key=lambda x:int(x.get('priority',100))):
        field=item.get('field'); op=item.get('operator','eq'); expected=item.get('value'); actual=variables.get(field)
        if op not in OPERATORS: raise ValueError('Operador inválido')
        if op=='eq': ok=actual==expected
        elif op=='ne': ok=actual!=expected
        elif op=='gt': ok=actual is not None and actual>expected
        elif op=='gte': ok=actual is not None and actual>=expected
        elif op=='lt': ok=actual is not None and actual<expected
        elif op=='lte': ok=actual is not None and actual<=expected
        elif op=='in': ok=actual in (expected if isinstance(expected,(list,tuple,set)) else [expected])
        elif op=='not_in': ok=actual not in (expected if isinstance(expected,(list,tuple,set)) else [expected])
        elif op=='contains': ok=str(expected) in str(actual or '')
        elif op=='starts_with': ok=str(actual or '').startswith(str(expected))
        elif op=='ends_with': ok=str(actual or '').endswith(str(expected))
        elif op=='is_empty': ok=actual in (None,'',[])
        else: ok=actual not in (None,'',[])
        if not ok: return False
    return True

def first_nodes(version)->list[str]:
    start=next(n for n in version.nodes if n.get('type')=='start')
    return [e['target'] for e in version.edges if e.get('source')==start['id']]

def due_date(config:dict|None):
    hours=int((config or {}).get('sla_hours',24)); return datetime.now(timezone.utc)+timedelta(hours=max(1,min(hours,8760)))
