import hashlib, json, math, re, time
from typing import Iterable
import httpx
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.entities import EnterpriseAIProvider, AIPolicy, KnowledgeBase, KnowledgeChunk, KnowledgeDocument, PromptTemplate, Role, User

WORD_RE=re.compile(r"[\wÀ-ÿ-]+",re.UNICODE)
DEFAULT_REDACTIONS=[r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b",r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b",r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}"]

def chunk_text(text:str,size:int=1200,overlap:int=180)->list[str]:
    clean=' '.join(text.replace('\x00',' ').split())
    if not clean: return []
    out=[]; start=0
    while start<len(clean):
        end=min(len(clean),start+size); piece=clean[start:end]
        if end<len(clean):
            cut=max(piece.rfind('. '),piece.rfind('\n'),piece.rfind(' '))
            if cut>size//2: end=start+cut+1; piece=clean[start:end]
        out.append(piece.strip());
        if end>=len(clean): break
        start=max(start+1,end-overlap)
    return out

def local_embedding(text:str,dimensions:int=256)->list[float]:
    vec=[0.0]*dimensions
    for token in WORD_RE.findall(text.lower()):
        digest=hashlib.sha256(token.encode()).digest(); idx=int.from_bytes(digest[:4],'big')%dimensions; sign=1 if digest[4]%2 else -1
        vec[idx]+=sign*(1+min(len(token),12)/12)
    norm=math.sqrt(sum(x*x for x in vec)) or 1.0
    return [round(x/norm,7) for x in vec]

def cosine(a:list[float],b:list[float])->float:
    return sum(x*y for x,y in zip(a,b))/(math.sqrt(sum(x*x for x in a))*math.sqrt(sum(y*y for y in b)) or 1.0)

def accessible_kb(kb:KnowledgeBase,u:User)->bool:
    return u.role in {Role.admin,Role.director,Role.data_steward} or kb.owner_id==u.id or (kb.department_id and kb.department_id==u.department_id) or u.role.value in (kb.allowed_roles or [])

def retrieve(db:Session,u:User,kb_ids:list[str],query:str,top_k:int=6)->list[dict]:
    qv=local_embedding(query); candidates=[]
    for kb_id in kb_ids:
        kb=db.get(KnowledgeBase,kb_id)
        if not kb or not kb.enabled or not accessible_kb(kb,u): continue
        rows=db.execute(select(KnowledgeChunk,KnowledgeDocument).join(KnowledgeDocument,KnowledgeChunk.document_id==KnowledgeDocument.id).where(KnowledgeDocument.knowledge_base_id==kb_id,KnowledgeDocument.status=='ready')).all()
        for chunk,doc in rows:
            score=cosine(qv,chunk.embedding or []) if chunk.embedding else 0
            candidates.append({'score':score,'chunk_id':chunk.id,'document_id':doc.id,'title':doc.title,'content':chunk.content,'metadata':chunk.metadata_json or {}})
    return sorted(candidates,key=lambda x:x['score'],reverse=True)[:max(1,min(top_k,20))]

def redact(text:str,patterns:Iterable[str])->tuple[str,list[str]]:
    flags=[]; out=text
    for idx,p in enumerate([*DEFAULT_REDACTIONS,*list(patterns or [])]):
        try:
            new,n=re.subn(p,'[DADO_PROTEGIDO]',out,flags=re.I)
            if n: flags.append(f'redaction_{idx}:{n}')
            out=new
        except re.error: flags.append(f'invalid_pattern_{idx}')
    return out,flags

def resolve_policy(db:Session,u:User)->AIPolicy|None:
    policies=db.scalars(select(AIPolicy).where(AIPolicy.enabled==True).order_by(AIPolicy.department_id.desc())).all()
    return next((p for p in policies if p.department_id==u.department_id and (not p.allowed_roles or u.role.value in p.allowed_roles)),None) or next((p for p in policies if p.department_id is None and (not p.allowed_roles or u.role.value in p.allowed_roles)),None)

def render_prompt(template:PromptTemplate|None,question:str,variables:dict)->str:
    text=template.template if template else '{{question}}'
    vals={**variables,'question':question}
    for k,v in vals.items(): text=text.replace('{{'+k+'}}',str(v))
    return text

def call_provider(provider:EnterpriseAIProvider,system:str,user_prompt:str,max_tokens:int)->tuple[str,dict]:
    from app.core.security import decrypt_secret
    cfg=json.loads(decrypt_secret(provider.encrypted_config)) if provider.encrypted_config else {}; typ=provider.provider_type; timeout=cfg.get('timeout',60)
    if typ=='mock': return f"Resposta de homologação baseada no contexto autorizado:\n\n{user_prompt[:1800]}",{'prompt_tokens':len(user_prompt)//4,'completion_tokens':100}
    if typ in {'openai','openai_compatible','local'}:
        base=(provider.base_url or 'https://api.openai.com/v1').rstrip('/'); headers={'Authorization':'Bearer '+cfg.get('api_key',''),'Content-Type':'application/json'}
        payload={'model':provider.model_name,'messages':[{'role':'system','content':system},{'role':'user','content':user_prompt}],'max_tokens':max_tokens,'temperature':cfg.get('temperature',0.2)}
        r=httpx.post(base+'/chat/completions',headers=headers,json=payload,timeout=timeout); r.raise_for_status(); data=r.json(); return data['choices'][0]['message']['content'],data.get('usage',{})
    if typ=='gemini':
        key=cfg.get('api_key',''); base=(provider.base_url or 'https://generativelanguage.googleapis.com/v1beta').rstrip('/'); url=f"{base}/models/{provider.model_name}:generateContent?key={key}"
        payload={'systemInstruction':{'parts':[{'text':system}]},'contents':[{'role':'user','parts':[{'text':user_prompt}]}],'generationConfig':{'maxOutputTokens':max_tokens,'temperature':cfg.get('temperature',0.2)}}
        r=httpx.post(url,json=payload,timeout=timeout); r.raise_for_status(); data=r.json(); text=''.join(p.get('text','') for p in data['candidates'][0]['content']['parts']); return text,data.get('usageMetadata',{})
    raise ValueError('Provedor não suportado')
