#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OPC 城市选址六维加权量化评分引擎 v2.0
======================================
依据《中国31城OPC创业选址白皮书》2026 附录C 评分规则复现。

八维权重(v4.0): D1市场0.22 / D2成本0.20 / D3产业0.18 / D4政策0.12 / D5人才0.08 / D6生活0.07 / D7数字基建算力0.08 / D8融资创投0.05
总分公式: total = (D1*0.22 + D2*0.20 + D3*0.18 + D4*0.12 + D5*0.08 + D6*0.07 + D7*0.08 + D8*0.05) * 10
评级:     S>=85 / A75-84 / B60-74 / C45-59 / D<45

v4.0 升级（2026-09）: 六维→八维，新增 D7数字基建与算力、D8融资与创投环境（资质型评分，来源：工信部先导区/发改委东数西算/科技部试验区/胡润独角兽）。

用途: ① 用已评分的 D1-D6 重算总分(校验白皮书数据)
      ② 用原始指标(附录C区间) 计算任意新城 D1-D6 -> 总分
      ③ 生成 TopN 排名(按总分排序; 默认全量导出)

2026-08-31 十轮自检重构（缺陷全闭环）:
- 删除 grade() 死代码(未定义变量town+if False恒假) 与 _grade_by_total() 中转
- _pick_band 输入防护(非数值/<=0 -> None) + 全区间表去重叠(D1边界双命中偏下已修)
- rank() 默认全量(防100截断) + total=None防护 + 同分稳定排序 + 不污染源对象
- normalize() 统一 D 键小写(根除历史"5城大写致六维空白"事故)
- validate 支持 31 城/103 城全量双校验
"""
import json, sys, os

WEIGHTS = {"D1": 0.22, "D2": 0.20, "D3": 0.18, "D4": 0.12, "D5": 0.08, "D6": 0.07, "D7": 0.08, "D8": 0.05}

# ===== D7/D8 资质名单（v4.0 八维扩展·来源标注） =====
# 国家人工智能创新应用先导区（工信部·三批）
AI_XIANDO = ['北京','上海','深圳','广州','杭州','南京','成都','武汉','长沙','天津','济南','青岛','苏州']
# 东数西算枢纽集群城市（国家发改委2022·103城内的集群城市）
DONGSHU = ['呼和浩特','上海','苏州','芜湖','贵阳','成都','重庆']
# 国家新一代人工智能创新发展试验区（科技部）
AI_SHIYAN = ['北京','上海','天津','深圳','杭州','合肥','广州','济南','成都','武汉','南京','沈阳','苏州','郑州','西安','重庆','长沙','青岛']
# 独角兽企业所在城市（胡润全球独角兽榜·分档）
DUJIAO_T1 = ['北京','上海','深圳','杭州']
DUJIAO_T2 = ['广州','南京','成都','武汉','苏州']
DUJIAO_T3 = ['合肥','西安','长沙','重庆','天津','青岛','厦门','无锡','常州','宁波','嘉兴']
# 创投/引导基金保底 = 省会 + 直辖市 + 计划单列市 + 苏州
SHENGHUI = ['石家庄','太原','呼和浩特','沈阳','长春','哈尔滨','南京','杭州','合肥','福州','南昌','济南','郑州','武汉','长沙','广州','南宁','海口','成都','贵阳','昆明','西安','兰州','西宁','银川','乌鲁木齐','拉萨']
ZHIXIA = ['北京','上海','天津','重庆']
JIHUA = ['大连','青岛','宁波','厦门','深圳']
BAODI = set(SHENGHUI + ZHIXIA + JIHUA + ['苏州'])
GANGAOTAI = ['香港','澳门','台湾省']

def d7_score(city):
    if city in GANGAOTAI: return None
    s = 2  # 基础分：所有城市均具备互联网/数字基础设施
    if city in AI_XIANDO: s += 3
    if city in DONGSHU: s += 3
    if city in AI_SHIYAN: s += 2
    return min(s, 10)

def d8_score(city):
    if city in GANGAOTAI: return None
    s = 2  # 基础分：所有城市均具备基础金融服务
    if city in DUJIAO_T1: s += 3
    elif city in DUJIAO_T2: s += 2
    elif city in DUJIAO_T3: s += 1
    if city in BAODI: s += 5
    return min(s, 10)

def total_from_d(d):
    return round((d["D1"]*WEIGHTS["D1"] + d["D2"]*WEIGHTS["D2"] + d["D3"]*WEIGHTS["D3"]
                 + d["D4"]*WEIGHTS["D4"] + d["D5"]*WEIGHTS["D5"] + d["D6"]*WEIGHTS["D6"]
                 + d.get("D7", 0)*WEIGHTS["D7"] + d.get("D8", 0)*WEIGHTS["D8"]) * 10, 1)

def grade_from_total(t):
    if t is None: return None
    if t >= 85: return "S"
    if t >= 75: return "A"
    if t >= 60: return "B"
    if t >= 45: return "C"
    return "D"

# ---------- 附录C：原始指标 -> 1-10 分映射（区间已去重叠：[lo,hi] 连续无缝隙） ----------
def _pick_band(value, table):
    """value(数值) 或 None；table 为 [(lo, hi, score)...]，(lo=0 表示 0<value<=hi)。
    防护：None / 非数值 / <=0 一律返回 None（引擎层不猜测、不兜底给分）。"""
    if value is None: return None
    if not isinstance(value, (int, float)) or value <= 0: return None
    for lo, hi, score in table:
        if lo == 0:
            if value <= hi: return score
        elif lo <= value <= hi:
            return score
    return None

# D1 市场空间：取 三项(重载后最高分) —— 区间边界已修：(0,5000,4)=4000→4分? 否: 0<v<=5000 得4; 5001-8000 得5
D1_GDP = [(0, 5000, 4), (5001, 8000, 5), (8001, 12000, 6),
          (12001, 18000, 7), (18001, 25000, 8), (25001, 40000, 9), (40001, 1e12, 10)]
D1_INC = [(0, 38000, 4), (38001, 42000, 4), (42001, 48000, 5), (48001, 55000, 6),
          (55001, 65000, 7), (65001, 75000, 8), (75001, 85000, 9), (85001, 1e12, 10)]
D1_RET = [(0, 1300, 4), (1301, 2200, 4), (2201, 3500, 5), (3501, 5000, 6),
          (5001, 7000, 7), (7001, 10000, 8), (10001, 15000, 9), (15001, 1e12, 10)]

# D2 成本压力：取 两项(较低分=短板)
D2_COST = [(0, 1500, 10), (1501, 2000, 9), (2001, 2500, 8), (2501, 3000, 7),
           (3001, 3500, 6), (3501, 4200, 5), (4201, 5000, 4), (5001, 1e12, 3)]
D2_SHEBAO = [(0, 3500, 10), (3501, 4200, 9), (4201, 5000, 8), (5001, 5800, 7),
             (5801, 6600, 6), (6601, 7500, 5), (7501, 8500, 4), (8501, 1e12, 3)]

# D3 产业适配度：三产占比梯度 + 数字经济占比(仅10/9/8分档) + OPC优势产业数(7/6/5档硬门槛)
def d3_score(tertiary_pct=None, digital_pct=None, opc_industries=None):
    if tertiary_pct is None: return None
    if tertiary_pct >= 75 and digital_pct is not None and digital_pct >= 40: return 10
    if tertiary_pct >= 70 and digital_pct is not None and digital_pct >= 35 and (opc_industries or 0) >= 5: return 9
    if tertiary_pct >= 65 and digital_pct is not None and digital_pct >= 30 and (opc_industries or 0) >= 4: return 8
    if tertiary_pct >= 60 and (opc_industries or 0) >= 3: return 7
    if tertiary_pct >= 55 and (opc_industries or 0) >= 2: return 6
    if tertiary_pct >= 50 and (opc_industries or 0) >= 1: return 5
    return 4

# D4 政策友好度（四级加总，满分10）
def d4_score(reg_days=None, online=None, subsidy=None, incubator=None):
    s = 0
    if reg_days is not None:
        s += 3 if reg_days <= 1 else (2 if reg_days <= 3 else (1 if reg_days <= 5 else 0))
    if online is not None:
        s += 2 if online == 2 else (1 if online == 1 else 0)
    if subsidy is not None:
        s += 3 if subsidy >= 50000 else (2 if subsidy >= 10000 else (1 if subsidy >= 1000 else 0))
    if incubator is not None:
        s += 2 if incubator >= 20 else (1 if incubator >= 5 else 0)
    return s

# D5 人才可用性：取(高校数/在校生)最高分
D5_UNIV = [(0, 3, 5), (4, 7, 5), (8, 14, 6), (15, 24, 7), (25, 39, 8), (40, 59, 9), (60, 1e12, 10)]
D5_STUDENTS = [(0, 9, 5), (10, 19, 5), (20, 39, 6), (40, 59, 7), (60, 89, 8), (90, 119, 9), (120, 1e12, 10)]

# D6 生活可负担性：房价收入比（缺失处理见方法论页：按报告等级均值推定并标注）
D6_HOUSE = [(0, 8, 10), (8.01, 12, 9), (12.01, 16, 8), (16.01, 20, 7),
            (20.01, 25, 6), (25.01, 30, 5), (30.01, 35, 4), (35.01, 40, 3), (40.01, 1e12, 2)]

def d1_score(gdp=None, income=None, retail=None):
    cands = [_pick_band(gdp, D1_GDP), _pick_band(income, D1_INC), _pick_band(retail, D1_RET)]
    cands = [c for c in cands if c is not None]
    return max(cands) if cands else None

def d2_score(cost=None, shebao=None):
    cands = [_pick_band(cost, D2_COST), _pick_band(shebao, D2_SHEBAO)]
    cands = [c for c in cands if c is not None]
    return min(cands) if cands else None

def d5_score(univ=None, students=None):
    cands = [_pick_band(univ, D5_UNIV), _pick_band(students, D5_STUDENTS)]
    cands = [c for c in cands if c is not None]
    return max(cands) if cands else None

def d6_score(house=None):
    return _pick_band(house, D6_HOUSE)

def compute_d(indicators):
    """从原始指标算出 D1-D6(大写键). indicators 为 dict."""
    return {
        "D1": d1_score(indicators.get("gdp"), indicators.get("income"), indicators.get("retail")),
        "D2": d2_score(indicators.get("opc_cost"), indicators.get("shebao")),
        "D3": d3_score(indicators.get("tertiary_pct"), indicators.get("digital_pct"), indicators.get("opc_industries")),
        "D4": d4_score(indicators.get("reg_days"), indicators.get("online"), indicators.get("subsidy"), indicators.get("incubator")),
        "D5": d5_score(indicators.get("universities"), indicators.get("students_wan")),
        "D6": d6_score(indicators.get("house_income_ratio")),
    }

def normalize(d):
    """统一评分键为小写 d1..d6（引擎输出大写->数据文件小写，导入前必须过此函数；
    历史事故：5城 D 键大写导致线上六维空白）。"""
    out = {}
    for k in ("d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8"):
        out[k] = d.get(k) if k in d else d.get(k.upper())
    return out

def score_ind(indicators):
    """一键算分: 原始指标 -> {d1..d6, total, grade}（全小写+评级, 与数据文件形状一致）"""
    dd = compute_d(indicators)
    total = None if all(v is None for v in dd.values()) else total_from_d(dd)
    merged = normalize(dd)
    merged["total"] = total
    merged["grade"] = grade_from_total(total)
    return merged

def validate_31(path):
    """重算总分并与白皮书给定值比对."""
    data = json.load(open(path, encoding="utf-8"))
    ok = 0; bad = []
    for c in data["cities"]:
        calc = total_from_d({"D1": c["d1"], "D2": c["d2"], "D3": c["d3"], "D4": c["d4"], "D5": c["d5"], "D6": c["d6"]})
        g = grade_from_total(calc)
        if abs(calc - c["total"]) < 0.051 and g == c["grade"]:
            ok += 1
        else:
            bad.append((c["city"], c["total"], calc, c["grade"], g))
    print(f"校验: {ok}/{len(data['cities'])} 城总分+评级完全复现")
    if bad:
        for b in bad: print("  缺差:", b)
    return ok == len(data["cities"]) and not bad

def validate_all():
    """全量校验: 31城复现 + 103城引擎重算与存储值一致."""
    c31 = validate_31(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "opc_cities_31.json"))
    allp = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "opc_cities_all.json")
    data = json.load(open(allp, encoding="utf-8"))
    ok = 0; bad = []
    for c in data["cities"]:
        # 存储值来自引擎/白皮书；仅对含 ind 的城重算比对（31权威城与新城均含 ind）
        if "ind" not in c: continue
        dd = compute_d(c["ind"])
        if any(v is None for v in dd.values()): continue
        calc = total_from_d(dd); g = grade_from_total(calc)
        if abs(calc - c["total"]) < 0.051 and g == c["grade"]:
            ok += 1
        else:
            bad.append((c["city"], c["total"], calc, c["grade"], g))
    print(f"全量校验: {ok}/{len(data['cities'])} 城（含ind且六维齐全）引擎重算一致")
    if bad:
        for b in bad: print("  缺差:", b)
    return c31 and not bad

def rank(limit=None, only="all", out=None):
    """从数据文件读所有城, 按总分降序生成排名(默认全量导出)。
    防护: total=None 排最后; 同分按 d1>d2>... 稳定; 不污染源字典。"""
    dd = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data")
    data = None; src = None
    for p in [os.path.join(dd, "opc_cities_all.json"), os.path.join(dd, "opc_cities_31.json")]:
        if os.path.exists(p):
            data = json.load(open(p, encoding="utf-8")); src = p; break
    if data is None:
        raise FileNotFoundError("无数据文件 (opc_cities_all.json / opc_cities_31.json)")
    cities = [dict(c) for c in data["cities"]]  # 拷贝: 不污染源
    def sort_key(c):
        t = c.get("total")
        return (0 if t is None else 1, -(t if t is not None else 0),
                -(c.get("d1") or 0), -(c.get("d2") or 0), -(c.get("d3") or 0),
                -(c.get("d4") or 0), -(c.get("d5") or 0), -(c.get("d6") or 0), c.get("city", ""))
    cities.sort(key=sort_key)
    for i, c in enumerate(cities, 1):
        c["rank"] = i
    if limit and limit < len(cities):
        cities = cities[:limit]
    if out:
        meta = dict(data.get("meta") or {})
        meta.setdefault("count", len(cities))
        json.dump({"meta": meta, "cities": cities}, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
        print(f"已导出 {len(cities)} 城排名 -> {out}")
    return cities

if __name__ == "__main__":
    base = os.path.dirname(os.path.abspath(__file__))
    datadir = os.path.join(base, "..", "data")
    data31 = os.path.join(datadir, "opc_cities_31.json")
    if len(sys.argv) > 1 and sys.argv[1] == "validate":
        arg = sys.argv[2] if len(sys.argv) > 2 else "all"
        if arg == "31":
            validate_31(data31)
        else:
            validate_all()
    elif len(sys.argv) > 1 and sys.argv[1] == "rank":
        n = int(sys.argv[2]) if len(sys.argv) > 2 else None
        rank(limit=n, out=os.path.join(datadir, "opc_rank.json"))
    elif len(sys.argv) > 1 and sys.argv[1] == "score":
        # 手动打分示例: python3 opc_rank.py score '{"gdp":32039,"income":83436,"opc_cost":2800,"shebao":5000,...}'
        ind = json.loads(sys.argv[2])
        r = score_ind(ind); print("D1-D6:", r); print("总分:", r["total"], "|评级:", r["grade"])
    else:
        validate_all()
        print("\n用法: validate [31|all] | rank [N] 导出TopN(默认全量) | score '<json指标>' 给新城打分")
