# -*- coding: utf-8 -*-
"""原创离线合成样例；不连接模型、不读取账户。输出位于脚本旁。
已有输入保留，修改输入后可以重跑。失败边界由 --negative 单独演示。
"""
from pathlib import Path
import sys, json, csv, math, io, hashlib, html, base64, re
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
BASE = Path(__file__).resolve().parent
NEG = '--negative' in sys.argv
def write(name, value):
    p=BASE/name; p.parent.mkdir(parents=True,exist_ok=True)
    if isinstance(value,bytes): p.write_bytes(value)
    else: p.write_text(value,encoding='utf-8',newline='')
def seed(name,value):
    if not (BASE/name).exists(): write(name,value)
def jwrite(name,value): write(name,json.dumps(value,ensure_ascii=False,indent=2))
def jseed(name,value):
    if not (BASE/name).exists(): jwrite(name,value)
def load(name): return json.loads((BASE/name).read_text(encoding='utf-8'))
def table(name,heads,rows):
    f=io.StringIO(newline='');w=csv.writer(f);w.writerow(heads);w.writerows(rows);write(name,f.getvalue())
def page(name,title,body):
    write(name,'<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+html.escape(title)+'</title><style>body{max-width:1000px;margin:2rem auto;padding:0 1rem;font:17px/1.7 system-ui}table{border-collapse:collapse}td,th{border:1px solid #aaa;padding:.5rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}img,svg{max-width:100%;height:auto}a{color:#125caa}</style><h1>'+html.escape(title)+'</h1>'+body+'</html>')
def emit(data): print(json.dumps(data,ensure_ascii=False))

seed('graded-qrels.csv','doc,grade\na,3\nb,2\nc,1\nd,0\n')
seed('rankings.csv','system,position,doc\nX,1,a\nX,2,b\nX,3,c\nY,1,c\nY,2,b\nY,3,a\n')
grades={r['doc']:int(r['grade']) for r in csv.DictReader((BASE/'graded-qrels.csv').open())}
if NEG: grades['a']=-1
if any(v<0 for v in grades.values()): raise ValueError('线性gain样例不接收负等级')
rankings=list(csv.DictReader((BASE/'rankings.csv').open()));k=3;ideal=sum(g/math.log2(i+2) for i,g in enumerate(sorted(grades.values(),reverse=True)[:k]));out=[];scores={}
for system in sorted({r['system'] for r in rankings}):
    ordered=sorted((r for r in rankings if r['system']==system),key=lambda r:int(r['position']))
    positions=[int(r['position']) for r in ordered];docs=[r['doc'] for r in ordered]
    if positions!=list(range(1,len(ordered)+1)):raise ValueError('position必须从1开始连续且不重复')
    if len(set(docs))!=len(docs):raise ValueError('同一排名doc不得重复')
    if any(doc not in grades for doc in docs):raise ValueError('排名doc没有人工相关等级')
    subtotal=0
    for r in ordered[:k]:
        pos=int(r['position']);g=grades[r['doc']];contrib=g/math.log2(pos+1);subtotal+=contrib;out.append([system,pos,r['doc'],g,math.log2(pos+1),contrib])
    scores[system]=None if ideal==0 else subtotal/ideal
table('ndcg-workbook.csv',['system','rank','doc','linear_gain','discount','contribution'],out);emit({'idcg':ideal,'ndcg':scores,'zero_idcg_policy':'undefined'})
