import os, io, csv, time, logging, json, hashlib
from pathlib import Path
from flask import Flask, render_template, jsonify, request, send_file, abort
from dotenv import load_dotenv
from engine import parse_csv, analyze
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
import requests
ROOT=Path(__file__).resolve().parent
# Read the .env next to server.py even if Windows has an older GEMINI_API_KEY
# already set as a persistent environment variable.
load_dotenv(ROOT/'.env', override=True)
app=Flask(__name__); app.config['MAX_CONTENT_LENGTH']=5*1024*1024
SOURCE=ROOT/'data'/'klaim_matriks_aktif(3).csv'
raw_rows,quality=parse_csv(SOURCE.read_bytes()); claims=analyze(raw_rows)
LAST={}
log=logging.getLogger('matrix_sentinel.ai')

def clean_key(v):
    return (v or '').strip().strip('\"').strip("'").strip()

def auth_fingerprint(v):
    return hashlib.sha256(clean_key(v).encode('utf-8')).hexdigest()[:12] if clean_key(v) else None


def current():return claims
@app.after_request
def secure(r):
    r.headers['X-Content-Type-Options']='nosniff';r.headers['X-Frame-Options']='DENY';r.headers['Referrer-Policy']='no-referrer';r.headers['Cache-Control']='no-store';r.headers['Content-Security-Policy']="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
    return r
@app.get('/')
def index():return render_template('index.html')
@app.get('/api/state')
def state():return jsonify({'claims':claims,'quality':quality,'source':'Data contoh dari unggahan pengguna (simulasi)','method':'Skor heuristik, belum ML terlatih; clinical twin eksploratif'})
@app.post('/api/upload')
def upload():
    global raw_rows,quality,claims
    f=request.files.get('file')
    if not f or not f.filename.lower().endswith('.csv'):return jsonify(error='Pilih CSV yang sesuai.'),400
    try:r,q=parse_csv(f.read());
    except (ValueError,UnicodeError,csv.Error) as e:return jsonify(error=str(e)),400
    if not r:return jsonify(error='Tidak ada baris klaim valid.'),400
    raw_rows,quality,claims=r,q,analyze(r)
    return state()
@app.post('/api/reset')
def reset():
    global raw_rows,quality,claims
    raw_rows,quality=parse_csv(SOURCE.read_bytes());claims=analyze(raw_rows)
    return state()
@app.get('/api/export.csv')
def export_csv():
    output=io.StringIO();w=csv.writer(output);w.writerow(['ID_Klaim','Diagnosis','Kelas','LOS','DxSek','Indeks_Biaya','Skor_Prioritas','Prioritas','Median_Pembanding','Jumlah_Pembanding'])
    for c in claims:w.writerow([c['id'],c['diagnosis'],c['kelas'],c['los'],c['ds'],c['biaya'],c['score'],c['risk'],c['peer_median'],c['peer_count']])
    return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')),mimetype='text/csv',as_attachment=True,download_name='audit_matrix_sentinel.csv')
@app.get('/api/report.pdf')
def export_pdf():
    b=io.BytesIO();p=canvas.Canvas(b,pagesize=A4);W,H=A4;y=H-56
    p.setFont('Helvetica-Bold',17);p.drawString(42,y,'Matrix-Sentinel JKN 2.0 - Audit Priority');y-=26
    p.setFont('Helvetica',9);p.drawString(42,y,'Prototype heuristik - temuan memerlukan verifikasi manusia; bukan bukti fraud.');y-=30
    p.drawString(42,y,f'Total klaim: {len(claims)} | Prioritas tinggi: {sum(x["risk"]=="Tinggi" for x in claims)}');y-=24
    for c in claims:
        if y<80:p.showPage();y=H-55
        p.setFont('Helvetica-Bold',10);p.drawString(42,y,f'{c["id"]} | Skor {c["score"]}/100 | {c["risk"]}');y-=15
        p.setFont('Helvetica',9);p.drawString(48,y,f'LOS {c["los"]} hari | DxSek {c["ds"]} | Index biaya {c["biaya"]} | Kode {c["code"]}');y-=19
    p.save();b.seek(0);return send_file(b,mimetype='application/pdf',as_attachment=True,download_name='laporan_matrix_sentinel.pdf')
@app.get('/api/sources')
def sources():
    return jsonify([{'name':f.name,'size':f.stat().st_size} for f in (ROOT/'data').glob('*.csv')])
@app.get('/api/sources/<path:name>')
def download_source(name):
    allowed={f.name:f for f in (ROOT/'data').glob('*.csv')}
    if name not in allowed:abort(404)
    return send_file(allowed[name],as_attachment=True,download_name=name)
@app.post('/api/chat')
def chat():
    payload=request.get_json(silent=True) or {}; question=str(payload.get('message') or '').strip()[:700]; selected=str(payload.get('claim_id') or '')[:100]
    if not question:return jsonify(error='Pertanyaan kosong.'),400
    ip=request.remote_addr or 'local';now=time.monotonic()
    cooldown=int(os.getenv('RATE_LIMIT_COOLDOWN','15')) if os.getenv('RATE_LIMIT_ENABLED','true').lower()=='true' else 0
    if now-LAST.get(ip,-1e10)<cooldown:return jsonify(error=f'Batas akses: coba lagi setelah {cooldown} detik.'),429
    LAST[ip]=now
    match=next((c for c in claims if c['id']==selected),None)
    if not match:
        match=next((c for c in claims if c['id'].lower() in question.lower()),None)
    brief=[{'claim_reference':c['id'],'risk':c['risk'],'score':c['score'],'los':c['los'],'secondary_count':c['ds'],'code':c['code'],'peer_median':c['peer_median'],'peer_count':c['peer_count'],'reasons':c['reasons']} for c in ( [match] if match else claims[:8] )]
    key=clean_key(os.getenv('GEMINI_API_KEY',''));enabled=os.getenv('ENABLE_EXTERNAL_AI','false').lower()=='true'
    if not key or not enabled:
        target=match or (claims[0] if claims else None)
        answer=('Mode penjelasan lokal (AI eksternal belum diaktifkan). ' + (f'Klaim {target["id"]}: prioritas {target["risk"]}, skor {target["score"]}/100. '+ ' '.join(target['reasons'])+' Auditor perlu memeriksa rekam medis, tindakan, dan dokumen pendukung.' if target else 'Tidak ada data.'))
        return jsonify(reply=answer,mode='local')
    base=os.getenv('GOOGLE_GEMINI_BASE_URL','https://api.sylorapi.com').strip().rstrip('/')
    # Accept a root URL, /v1, or a fully specified /chat/completions URL.
    if base.endswith('/chat/completions'):
        url=base
    elif base.endswith('/v1'):
        url=base+'/chat/completions'
    else:
        url=base+'/v1/chat/completions'
    model=os.getenv('GEMINI_MODEL','gemini-3.5-flash').strip()
    system='Anda asisten audit JKN. Gunakan HANYA data JSON yang diberikan, jangan menyatakan fraud terbukti, jangan mengarang bukti atau standar klinis. Penilaian heuristik, bukan probabilitas fraud. Jawab Bahasa Indonesia dan sebut keterbatasan sampel.'
    data={'model':model,'messages':[{'role':'system','content':system+' DATA:'+json.dumps(brief,ensure_ascii=False)},{'role':'user','content':question}],'temperature':0.2,'max_tokens':500}
    try:
        response=requests.post(url,json=data,headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'},timeout=min(120,max(5,int(os.getenv('GEMINI_TIMEOUT','120')))))
    except requests.exceptions.Timeout:
        log.warning('AI timeout (%s)', model)
        return jsonify(error='AI tidak merespons dalam batas waktu. Periksa koneksi dan GEMINI_TIMEOUT.',code='AI_TIMEOUT'),504
    except requests.exceptions.RequestException as exc:
        log.warning('AI network error: %s',type(exc).__name__)
        return jsonify(error='Gagal terhubung ke penyedia AI. Periksa URL, internet, firewall, dan sertifikat HTTPS.',code='AI_NETWORK'),502
    if response.status_code>=400:
        code=response.status_code
        log.warning('AI provider HTTP %s (model=%s)',code,model)
        if code in (401,403): message='Kunci API tidak diterima atau akun tidak memiliki izin untuk model ini.'
        elif code==404: message='Endpoint atau model tidak ditemukan. Periksa URL API dan nama model pada penyedia.'
        elif code==429: message='Batas kuota/rate limit penyedia AI tercapai. Coba lagi kemudian.'
        elif code>=500: message='Server penyedia AI mengalami gangguan. Coba lagi kemudian.'
        elif code==400: message='Permintaan ditolak penyedia (HTTP 400). Kemungkinan parameter atau nama model tidak kompatibel.'
        else: message='Penyedia AI menolak permintaan.'
        return jsonify(error=f'{message} (HTTP {code})',code='AI_HTTP_ERROR',http_status=code),502
    try:
        result=response.json()
    except ValueError:
        log.warning('AI response not JSON, status %s',response.status_code)
        return jsonify(error='Penyedia AI mengirim respons bukan JSON. Periksa kompatibilitas endpoint API.',code='AI_NOT_JSON'),502
    content=None
    try:
        content=result['choices'][0]['message']['content']
    except (KeyError,IndexError,TypeError):
        # Gemini native responses, if provider returns this shape instead.
        try:
            parts=result['candidates'][0]['content']['parts']
            content='\n'.join(str(part.get('text','')) for part in parts)
        except (KeyError,IndexError,TypeError,AttributeError):
            pass
    if isinstance(content,list):
        content='\n'.join(str(x.get('text','')) for x in content if isinstance(x,dict))
    if not isinstance(content,str) or not content.strip():
        log.warning('Unexpected AI response keys: %s',sorted(result.keys()) if isinstance(result,dict) else type(result).__name__)
        return jsonify(error='Respons AI diterima, tetapi tidak memiliki teks jawaban dalam format yang dikenali.',code='AI_FORMAT'),502
    return jsonify(reply=content.strip(),mode='external')

@app.get('/api/ai/diagnostics')
def ai_diagnostics():
    # No network call and no token disclosed. Compare hash prefix from a local PowerShell check.
    from dotenv import dotenv_values
    disk_key=clean_key(dotenv_values(ROOT/'.env').get('GEMINI_API_KEY')) if (ROOT/'.env').is_file() else ''
    runtime_key=clean_key(os.getenv('GEMINI_API_KEY'))
    return jsonify(env_exists=(ROOT/'.env').is_file(),env_path=str(ROOT/'.env'),
                   process_id=os.getpid(),runtime_key_present=bool(runtime_key),
                   runtime_key_fingerprint=auth_fingerprint(runtime_key),
                   file_key_fingerprint=auth_fingerprint(disk_key),
                   runtime_matches_file=bool(runtime_key) and runtime_key==disk_key,
                   active_model=os.getenv('GEMINI_MODEL',''),
                   note='Fingerprint SHA256 terpotong untuk diagnosis lokal; jangan publikasikan endpoint ini ke internet.')

@app.get('/api/ai/status')
def ai_status():
    base=os.getenv('GOOGLE_GEMINI_BASE_URL','https://api.sylorapi.com')
    # Do not disclose the key or external URL details beyond its configured host.
    from urllib.parse import urlparse
    return jsonify(config_file_found=(ROOT/'.env').is_file(),config_file=str(ROOT/'.env'),process_id=os.getpid(),enabled=os.getenv('ENABLE_EXTERNAL_AI','false').lower()=='true',key_configured=bool(os.getenv('GEMINI_API_KEY','').strip()),model=os.getenv('GEMINI_MODEL','gemini-3.5-flash'),provider_host=urlparse(base).hostname,timeout=os.getenv('GEMINI_TIMEOUT','120'),rate_limit_enabled=os.getenv('RATE_LIMIT_ENABLED','true').lower()=='true',cooldown_seconds=os.getenv('RATE_LIMIT_COOLDOWN','15'),note='Status konfigurasi saja; tidak menguji koneksi langsung.')
if __name__=='__main__':app.run(host='127.0.0.1',port=5000,debug=False)
