from __future__ import annotations
import os,re,time
from pathlib import Path
from dataclasses import dataclass
from typing import Any
import httpx
ROOT=Path(__file__).resolve().parents[2]
_env=ROOT/'.env'
if _env.exists():
    for line in _env.read_text(encoding='utf-8').splitlines():
        line=line.strip()
        if line and not line.startswith('#') and '=' in line:
            k,v=line.split('=',1);os.environ.setdefault(k.strip(),v.strip().strip('"').strip("'"))
@dataclass
class ProviderSpec: key:str;label:str;base_url:str;default_model:str;api_key_env:str
PROVIDERS={
 'mock':ProviderSpec('mock','Local Mock (no API key)','local://mock','rp-mock-v1',''),
 'cloudflare':ProviderSpec('cloudflare','Cloudflare Workers AI','', '@cf/qwen/qwen3-30b-a3b-fp8','CLOUDFLARE_API_TOKEN'),
 'openrouter':ProviderSpec('openrouter','OpenRouter','https://openrouter.ai/api/v1','deepseek/deepseek-v4-flash','OPENROUTER_API_KEY'),
 'modal-pro':ProviderSpec('modal-pro','MelumiyaChat Modal — Pro',os.getenv('MELUMIYACHAT_MODAL_PRO_BASE_URL',''),os.getenv('MELUMIYACHAT_MODAL_PRO_MODEL','qwen3.6-35b-a3b-fp8'),'MELUMIYACHAT_MODAL_API_KEY'),
 'modal-ultra':ProviderSpec('modal-ultra','MelumiyaChat Modal — Ultra',os.getenv('MELUMIYACHAT_MODAL_ULTRA_BASE_URL',''),os.getenv('MELUMIYACHAT_MODAL_ULTRA_MODEL','qwen3.5-122b-a10b-fp8'),'MELUMIYACHAT_MODAL_API_KEY'),
 'deepseek':ProviderSpec('deepseek','DeepSeek direct',os.getenv('DEEPSEEK_BASE_URL','https://api.deepseek.com'),os.getenv('DEEPSEEK_MODEL','deepseek-chat'),'DEEPSEEK_API_KEY'),
 'qwen':ProviderSpec('qwen','Qwen / Alibaba Model Studio',os.getenv('QWEN_BASE_URL','https://dashscope-intl.aliyuncs.com/compatible-mode/v1'),os.getenv('QWEN_MODEL','qwen-plus'),'DASHSCOPE_API_KEY'),
 'custom':ProviderSpec('custom','Custom OpenAI-compatible',os.getenv('CUSTOM_BASE_URL','http://127.0.0.1:11434/v1'),os.getenv('CUSTOM_MODEL','local-model'),'CUSTOM_API_KEY')}
def public_provider_info():
    out=[]
    for s in PROVIDERS.values():
        ready=True if s.key=='mock' else bool((os.getenv('CLOUDFLARE_ACCOUNT_ID') and os.getenv('CLOUDFLARE_API_TOKEN')) if s.key=='cloudflare' else (s.base_url if s.key.startswith('modal-') else os.getenv(s.api_key_env)))
        out.append({'key':s.key,'label':s.label,'base_url':s.base_url,'default_model':s.default_model,'api_key_configured':ready,'api_key_env':s.api_key_env})
    return out
def _strip(text):return re.sub(r'<think>.*?</think>\s*','',str(text or ''),flags=re.I|re.S).strip()
def call_chat(provider,model,messages,temperature=.9,max_tokens=1200,base_url_override=None):
    s=PROVIDERS.get(provider)
    if not s: raise ValueError(f'Unknown provider: {provider}')
    model=model or s.default_model
    if provider=='mock':
        last=next((m['content'] for m in reversed(messages) if m['role']=='user'),'');return {'content':f'[MOCK v0.9 server-routed generation]\n\nThe active character responds naturally to “{last[:220]}” while preserving scene state and user agency.','latency_ms':5,'model':model,'provider':provider,'usage':{'prompt_tokens':0,'completion_tokens':0}}
    if provider=='cloudflare':
        acct=os.getenv('CLOUDFLARE_ACCOUNT_ID','').strip();key=os.getenv('CLOUDFLARE_API_TOKEN','').strip()
        if not acct or not key: raise RuntimeError('Cloudflare Workers AI credentials are not configured.')
        msgs=[dict(x) for x in messages]
        if model=='@cf/qwen/qwen3-30b-a3b-fp8':
            for i in range(len(msgs)-1,-1,-1):
                if msgs[i].get('role')=='user':msgs[i]['content']=str(msgs[i].get('content') or '')+'\n\n/no_think';break
        url=f'https://api.cloudflare.com/client/v4/accounts/{acct}/ai/v1/chat/completions';payload={'model':model,'messages':msgs,'temperature':temperature,'top_p':.95,'max_tokens':max_tokens,'stream':False};headers={'Authorization':f'Bearer {key}','Content-Type':'application/json'}
    else:
        base=(base_url_override or s.base_url).rstrip('/'); key=os.getenv(s.api_key_env,'').strip()
        if not base: raise RuntimeError(f'{provider} endpoint is not configured yet.')
        if not key and not provider.startswith('modal-'): raise RuntimeError(f'{s.api_key_env} is not configured.')
        url=base+'/chat/completions';payload={'model':model,'messages':messages,'temperature':temperature,'max_tokens':max_tokens};headers={'Content-Type':'application/json'}
        if key:headers['Authorization']=f'Bearer {key}'
        if provider=='openrouter':payload['reasoning']={'enabled':False};headers['HTTP-Referer']=os.getenv('MELUMIYACHAT_PUBLIC_URL','https://melumiya.pages.dev/');headers['X-Title']='MelumiyaChat'
    started=time.perf_counter()
    with httpx.Client(timeout=150) as c:r=c.post(url,headers=headers,json=payload)
    latency=int((time.perf_counter()-started)*1000)
    if r.status_code>=400:raise RuntimeError(f'Provider error {r.status_code}: {r.text[:1200]}')
    d=r.json();
    try:content=_strip(d['choices'][0]['message']['content'])
    except Exception as e:raise RuntimeError(f'Unexpected provider response: {str(d)[:1500]}') from e
    if not content:raise RuntimeError('Provider returned a blank visible reply.')
    # Cloudflare Qwen benchmark-equivalent list-price estimate; other providers may expose cost separately later.
    usage=d.get('usage') or {};cost=0.0
    if provider=='cloudflare' and model=='@cf/qwen/qwen3-30b-a3b-fp8':cost=(float(usage.get('prompt_tokens') or 0)*.051+float(usage.get('completion_tokens') or 0)*.335)/1_000_000
    return {'content':content,'latency_ms':latency,'model':model,'provider':provider,'usage':usage,'estimated_cost_usd':cost}
