#!/usr/bin/env python3
"""shop-analysis 确定性计算层。SKILL.md 调它做全部算术——判据、分母、退化过滤。
只依赖 stdlib。阈值集中在 THRESHOLDS（spec §10，全部为待调初值）。"""
import json, gzip, sys, argparse
from collections import Counter
from datetime import datetime, timedelta, timezone

THRESHOLDS = dict(T_lowsold=100, P_lowsold=0.30, T_bsr=100000, P_bsr=0.30,
                  P_brand=0.90, T_buybox=95, T_subscore=90,
                  T_review_lo=0.005, T_review_hi=0.10, T_price_spread=8.0,
                  K_maxsku=300, N_top=5, N_bottom=3)
KEEPA_EPOCH = datetime(2011, 1, 1, tzinfo=timezone.utc)  # §6.0b

def load_cache(path):
    op = gzip.open if path.endswith('.gz') else open
    with op(path, 'rt') as f:
        return json.load(f)

def amazon_seller(cache):
    sellers = cache['data']['data']['sellers']          # §6.0：三层，sellerId 为键的 map
    sid = next(iter(sellers))
    return sid, sellers[sid]

def amazon_products(cache):
    return cache['data']['data']['products']            # §6.0：三层

def tiktok_shop(cache):
    d = cache['data']                                   # §6.0：两层
    return d['shop'], d['products']

def keepa_min_to_iso(m):
    return (KEEPA_EPOCH + timedelta(minutes=m)).strftime('%Y-%m-%dT%H:%MZ')

def _current(product, idx):
    cur = (product.get('stats') or {}).get('current') or []
    v = cur[idx] if len(cur) > idx else None
    return v if isinstance(v, (int, float)) and v > 0 else None  # -1/0/缺 → None（§5.2）

def clean_price_cents(product): return _current(product, 1)
def clean_bsr(product): return _current(product, 3)

def clean_tiktok_price(product):
    v = product.get('price')
    return v if isinstance(v, (int, float)) and v > 0 else None  # 0 = 上游解析失败哨兵（§10.2）

def tiktok_rating_coverage(products):
    return {'total': len(products),
            'rating_nonnull': sum(1 for p in products if p.get('rating') is not None),
            'reviewCount_nonnull': sum(1 for p in products if p.get('reviewCount') is not None)}

def _j(jid, triggered=False, skipped=None, values=None, note=''):
    return {'id': jid, 'triggered': bool(triggered), 'skipped': skipped,
            'values': values or {}, 'note': note}

_NOTES = {  # spec §7 第三列，逐字
    'J2': '不能推出滞销原因；monthlySold 是 Keepa 的估计值不是真实销量',
    'J2b': '不能跨类目比较；不能换算成销量（要销量用 J2 的 monthlySold）',
    'J3': '不能推出产品线宽度不足——单品牌是常见且中性的经营形态',
    'J4': '不能推出损失了多少订单',
    'J5': '不能推出退款率/实际履约表现',
    'J6': '不能推出销量趋势',
    'J7': '不能推出定价策略对错',
}
_SUBSCORE_NAMES = {1: '24h 响应率', 3: '4 星及以上评价占比'}   # §5.1/§7.2：type 置换，别照搬 PDP

def judge_amazon(seller, products):
    T = THRESHOLDS
    out = []
    # J2 低销量（分母 = 有 monthlySold 的 SKU 数，§7.3）
    ms = [p['monthlySold'] for p in products if isinstance(p.get('monthlySold'), (int, float))]
    # J2 观察句式的尾巴（spec §7）：「该卖家在 catId=<C> 下的 avg30SalesRank 为 Z」，
    # <C> = sellerCategoryStatistics 中 productCount 最大那条
    cat_stats = seller.get('sellerCategoryStatistics') or []
    top_cat = max(cat_stats, key=lambda c: c.get('productCount') or 0) if cat_stats else None
    if not ms:
        out.append(_j('J2', skipped='无 monthlySold 数据（products 未取或全缺）', note=_NOTES['J2']))
    else:
        low = sum(1 for v in ms if v < T['T_lowsold'])
        out.append(_j('J2', triggered=(low / len(ms)) > T['P_lowsold'],
                      values={'low': low, 'denom': len(ms), 'T_lowsold': T['T_lowsold'],
                              'top_cat_id': top_cat.get('catId') if top_cat else None,
                              'top_cat_avg30SalesRank': top_cat.get('avg30SalesRank') if top_cat else None},
                      note=_NOTES['J2']))
    # J2b 同类目长尾（组内分母，§7.3 例外；先剔 -1，§5.2）
    groups = {}
    for p in products:
        bsr = clean_bsr(p)
        if bsr is None:
            continue
        ref = p.get('salesRankReference')
        cat = next((c.get('name') for c in (p.get('categoryTree') or []) if c.get('catId') == ref), str(ref))
        groups.setdefault(ref, {'cat': cat, 'bsrs': []})['bsrs'].append(bsr)
    trig_groups = []
    for ref, g in groups.items():
        share = sum(1 for b in g['bsrs'] if b > T['T_bsr']) / len(g['bsrs'])
        if share > T['P_bsr']:
            trig_groups.append({'ref': ref, 'cat': g['cat'], 'share': round(share, 4),
                                'count': sum(1 for b in g['bsrs'] if b > T['T_bsr']),  # 句式的「N 个品」
                                'n': len(g['bsrs']), 'T_bsr': T['T_bsr']})
    if not groups:
        out.append(_j('J2b', skipped='无有效排名数据', note=_NOTES['J2b']))
    else:
        out.append(_j('J2b', triggered=bool(trig_groups), values={'groups': trig_groups}, note=_NOTES['J2b']))
    # J3 单品牌（分子=逐 ASIN brand 计数，分母=有 brand 的 SKU 数并声明覆盖率，§7.3；禁用 seller*Statistics）
    brands = [p['brand'] for p in products if p.get('brand')]
    if not brands:
        out.append(_j('J3', skipped='无 brand 数据', note=_NOTES['J3']))
    else:
        top = Counter(brands).most_common(1)[0]
        out.append(_j('J3', triggered=(top[1] / len(brands)) > T['P_brand'],
                      values={'brand': top[0], 'share': round(top[1] / len(brands), 4),
                              'coverage': f'{len(brands)}/{len(products)}'}, note=_NOTES['J3']))
    # J4 购物车（seller 层标量 <0 → 视为无数据，§10.2 防御）
    rate = seller.get('buyBoxNewOwnershipRate')
    if not isinstance(rate, (int, float)) or rate < 0:
        out.append(_j('J4', skipped='无购物车占有率数据', note=_NOTES['J4']))
    else:
        out.append(_j('J4', triggered=rate < T['T_buybox'], values={'rate': rate}, note=_NOTES['J4']))
    # J6 评价活跃（分母 0 → 跳过，绝不 NaN）
    cnt = seller.get('currentRatingCount')
    r30 = seller.get('ratingsLast30Days')
    if not cnt or not isinstance(r30, (int, float)):
        out.append(_j('J6', skipped='currentRatingCount 为 0 或缺失（该店无评价数据，走 §5 类别 C）', note=_NOTES['J6']))
    else:
        ratio = r30 / cnt
        out.append(_j('J6', triggered=ratio < T['T_review_lo'] or ratio > T['T_review_hi'],
                      values={'r30': r30, 'cnt': cnt, 'ratio': ratio}, note=_NOTES['J6']))
    # J7 价格跨度（先剔 -1/0，§5.2；集合 <2 → 跳过）
    prices = [v for v in (clean_price_cents(p) for p in products) if v is not None]
    out.append(_price_spread_judgement(prices, unit_div=100))
    return out

def judge_tiktok(shop, products):
    T = THRESHOLDS
    out = []
    # J5 服务分（排除 type2 与 0/0 项，§7.1；名字按 §5.1 置换映射与 §7.2）
    sub = shop.get('storeSubScore')
    if not isinstance(sub, list) or not sub:
        out.append(_j('J5', skipped='storeSubScore 缺失（PR #185 未部署或上游未给）', note=_NOTES['J5']))
    else:
        entries = [{'type': e.get('type'), 'pct': e.get('scorePercentage'),
                    'name': _SUBSCORE_NAMES.get(e.get('type'), f"type{e.get('type')}")}
                   for e in sub
                   if e.get('type') != 2 and not (e.get('scorePercentage') == 0 and e.get('score') == 0)]
        bad = [e for e in entries if isinstance(e['pct'], (int, float)) and e['pct'] < T['T_subscore']]
        out.append(_j('J5', triggered=bool(bad), values={'entries': entries, 'below': bad}, note=_NOTES['J5']))
    # J7
    prices = [v for v in (clean_tiktok_price(p) for p in products) if v is not None]
    out.append(_price_spread_judgement(prices, unit_div=1))
    return out

def _price_spread_judgement(prices, unit_div):
    if len(prices) < 2:
        return _j('J7', skipped='有效价格不足 2 个', note=_NOTES['J7'])
    spread = max(prices) / min(prices)
    return _j('J7', triggered=spread > THRESHOLDS['T_price_spread'],
              values={'min': min(prices) / unit_div, 'max': max(prices) / unit_div,
                      'spread': spread}, note=_NOTES['J7'])

def _visibility(m_fetched, m_listed, ts_iso, addenda):
    if isinstance(m_listed, (int, float)) and m_listed >= m_fetched and m_listed > 0:
        return {'branch': 'normal', 'm_fetched': m_fetched, 'm_listed': m_listed,
                'listed_ts_iso': ts_iso, 'pct': m_fetched / m_listed, 'addenda': addenda}
    return {'branch': 'listed_unavailable', 'm_fetched': m_fetched,
            'm_listed': m_listed, 'listed_ts_iso': ts_iso, 'pct': None, 'addenda': addenda}

def visibility_amazon(seller):
    al = seller.get('asinList') or []
    tsa = seller.get('totalStorefrontAsins') or [None, None]
    ts_iso = keepa_min_to_iso(tsa[0]) if isinstance(tsa[0], int) and tsa[0] > 0 else None
    addenda = []
    if len(al) > THRESHOLDS['K_maxsku']:
        addenda.append(f"该店 SKU 超出本次分析上限，已按 §6.1b 预筛取 {THRESHOLDS['K_maxsku']} 个；该子集不具代表性")
    if len(al) == 100000:
        addenda.append('命中 Keepa 单次返回上限，实际 SKU 数未知')
    return _visibility(len(al), tsa[1], ts_iso, addenda)

def visibility_tiktok(shop, products):
    return _visibility(len(products), shop.get('onSellProductCount'), None, [])

def quantile_linear(values, q):
    """R type-7 / numpy 默认 linear（§8.2 钉死；别换 method——同组数 higher 与 linear 差一倍）"""
    v = sorted(values); n = len(v)
    if n == 1:
        return float(v[0])
    h = (n - 1) * q; lo = int(h); frac = h - lo
    return v[lo] + (v[min(lo + 1, n - 1)] - v[lo]) * frac

def price_stats(prices):
    """②画像与 §8.2 对比矩阵的价格统计——五分位 + IQR÷中位数，全部脚本算不心算"""
    if len(prices) < 2:
        return None
    qs = {f'P{int(q * 100)}': round(quantile_linear(prices, q), 2) for q in (0.10, 0.25, 0.50, 0.75, 0.90)}
    med = quantile_linear(prices, 0.50)
    iqr = quantile_linear(prices, 0.75) - quantile_linear(prices, 0.25)
    return {'quantiles': qs, 'iqr_over_median': (iqr / med) if med else None}

def concentration(items):
    """品牌/类目集中度：Top1 计数 ÷ 有值项数，附覆盖率（§7.3 分母纪律）"""
    vals = [x for x in items if x]
    if not vals:
        return None
    top = Counter(vals).most_common(1)[0]
    return {'top': top[0], 'share': top[1] / len(vals), 'coverage': f'{len(vals)}/{len(items)}'}

def sp_sample(products):
    """§6.3 opt-in 抽样：monthlySold 优先（降序=头部高销），全缺才退 current[3]（升序=头部好排名，
    已剔 -1）；头 N_top + 尾 N_bottom，尾部去重。返回 None = 无可排序数据。"""
    ms = [(p['monthlySold'], p.get('asin')) for p in products
          if isinstance(p.get('monthlySold'), (int, float)) and p.get('asin')]
    if ms:
        ranked = [a for _, a in sorted(ms, key=lambda t: -t[0])]
    else:
        bs = [(clean_bsr(p), p.get('asin')) for p in products if clean_bsr(p) is not None and p.get('asin')]
        if not bs:
            return None
        ranked = [a for _, a in sorted(bs, key=lambda t: t[0])]
    top = ranked[:THRESHOLDS['N_top']]
    bottom = [a for a in ranked[-THRESHOLDS['N_bottom']:] if a not in top]
    return {'top': top, 'bottom': bottom}

def gap_share(vis):
    """枚举缺口占比（§8.2 矩阵指标）；listed 不可用时返回 None——绝不除零"""
    if vis['branch'] != 'normal':
        return None
    return (vis['m_listed'] - vis['m_fetched']) / vis['m_listed']

def prefilter_asins(asin_list, last_seen, k):
    assert len(asin_list) == len(last_seen), 'asinList 与 asinListLastSeen 长度不等（§6.1b 前提破裂，停止预筛）'
    order = sorted(range(len(asin_list)), key=lambda i: (-last_seen[i], i))  # §6.1b：第二键破并列
    return [asin_list[i] for i in order[:k]]

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--platform', required=True, choices=['amazon', 'tiktok'])
    ap.add_argument('--seller-cache'); ap.add_argument('--products-cache', action='append')
    ap.add_argument('--shop-cache')
    a = ap.parse_args()
    if a.platform == 'amazon' and not a.seller_cache:
        ap.error('--platform amazon requires --seller-cache')
    if a.platform == 'tiktok' and not a.shop_cache:
        ap.error('--platform tiktok requires --shop-cache')
    if a.platform == 'amazon':
        _, seller = amazon_seller(load_cache(a.seller_cache))
        products = []
        for f in (a.products_cache or []):                 # 分批取数（spec §2 步骤3/4）：按顺序拼接，不是取最后一批
            products.extend(amazon_products(load_cache(f)))
        vis = visibility_amazon(seller)
        prices = [v / 100 for v in (clean_price_cents(p) for p in products) if v is not None]
        leaves = [(p.get('categoryTree') or [{}])[-1].get('name') for p in products]
        asin_list = seller.get('asinList') or []
        last_seen = seller.get('asinListLastSeen')
        needed = len(asin_list) > THRESHOLDS['K_maxsku']
        # asinListLastSeen 缺失或长度不符时防御性跳过预筛输出，而不是让 prefilter_asins 里的 assert 炸穿
        prefilter_usable = needed and isinstance(last_seen, list) and len(last_seen) == len(asin_list)
        out = {'visibility': vis,
               'judgements': judge_amazon(seller, products),
               'price_stats': price_stats(prices),
               'brand_concentration': concentration([p.get('brand') for p in products]),
               'category_concentration': concentration(leaves),
               'gap_share': gap_share(vis),
               'sp_sample': sp_sample(products),
               'prefilter_needed': needed,
               'prefilter_asins': (prefilter_asins(asin_list, last_seen, THRESHOLDS['K_maxsku'])
                                   if prefilter_usable else None)}
    else:
        shop, products = tiktok_shop(load_cache(a.shop_cache))
        vis = visibility_tiktok(shop, products)
        prices = [v for v in (clean_tiktok_price(p) for p in products) if v is not None]
        out = {'visibility': vis,
               'judgements': judge_tiktok(shop, products),
               'price_stats': price_stats(prices),
               'gap_share': gap_share(vis),
               'coverage': tiktok_rating_coverage(products)}
    json.dump(out, sys.stdout, ensure_ascii=False, indent=2, default=str)

if __name__ == '__main__':
    main()
