#!/usr/bin/env python3
"""am 页面表格分页注入（零依赖、幂等、纯前端）。

am (answer-me-with-html) 没有分页组件。LXXG 盘后选股网页的 ④ 观察档(281 行) +
⑤ 五日跟踪(974 行) 把页面拉到两万像素量级。本模块在 am 渲染完成后做一次增强：

  · 行数 >= min_rows 的表格自动切成每页 page_size 行
  · 表格上方插入控制条：上一页 / 页码跳转 / 下一页 / 每页行数(含"全部行")
  · 打印时自动展开全部行（@media print 覆盖内联 display:none）
  · 所有行仍留在页面 DOM 里，只隐藏非当前页 → 不违反"网页版必须全列"

CSP：am 页面用 sha256 白名单锁死 script-src，注入的脚本必须把自身 hash 写进
<meta http-equiv="Content-Security-Policy">，否则被浏览器直接拦掉。

用法:
    python3 am_paginate.py <page.html> [--page-size 50] [--min-rows 26]
    from am_paginate import paginate_file
    info = paginate_file('/root/web_reports/lxxg_daily_20261028.html')
    # -> {'injected': True, 'removed_old': False, 'tables_eligible': 2, 'page_size': 50}
"""
import argparse
import base64
import hashlib
import re
import sys
from pathlib import Path

MARK = '<script data-am-paginate="v1">'
SCRIPT_TMPL = """(function(){
  var MIN=__MIN_ROWS__, FIRST=__PAGE_SIZE__;
  var SIZES=[[50,'每页 50 行'],[100,'每页 100 行'],[200,'每页 200 行'],[0,'全部行']];
  var st=document.createElement('style');
  st.textContent='.am-pager{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 2px;font-size:12px;color:var(--ink-2);font-family:var(--font-sans)}'
   +'.am-pager button,.am-pager select{font:inherit;font-size:12px;color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:var(--radius);padding:3px 10px;cursor:pointer}'
   +'.am-pager button:disabled{opacity:.4;cursor:default}'
   +'.am-pager .am-pager-info{margin-left:auto;color:var(--ink-3)}'
   +'@media print{.am-pager{display:none}.am-paged-table tbody tr{display:table-row!important}}';
  document.head.appendChild(st);
  function mk(tag,cls,txt){var e=document.createElement(tag);if(cls)e.className=cls;if(txt!=null)e.textContent=txt;return e;}
  function initTable(t){
    var tb=t.tBodies[0];
    if(!tb) return 0;
    var rows=[]; for(var j=0;j<tb.rows.length;j++) rows.push(tb.rows[j]);
    if(rows.length<MIN) return 0;
    t.classList.add('am-paged-table');
    var size=FIRST, page=1, total=rows.length;
    var pages=function(){return size?Math.ceil(total/size):1;};
    var bar=mk('div','am-pager');
    var prev=mk('button',null,'‹ 上一页'); prev.type='button';
    var next=mk('button',null,'下一页 ›'); next.type='button';
    var pick=document.createElement('select');
    var siz=document.createElement('select');
    var info=mk('span','am-pager-info');
    for(var s=0;s<SIZES.length;s++){
      var op=document.createElement('option');
      op.value=String(SIZES[s][0]); op.textContent=SIZES[s][1];
      if(SIZES[s][0]===FIRST) op.selected=true;
      siz.appendChild(op);
    }
    function render(){
      var p=pages(); if(page>p)page=p; if(page<1)page=1;
      var from=size?(page-1)*size:0, to=size?Math.min(from+size,total):total;
      for(var k=0;k<total;k++) rows[k].style.display=(k>=from&&k<to)?'':'none';
      while(pick.firstChild) pick.removeChild(pick.firstChild);
      for(var q=1;q<=p;q++){
        var op2=document.createElement('option');
        op2.value=String(q); op2.textContent='第 '+q+'/'+p+' 页';
        if(q===page) op2.selected=true;
        pick.appendChild(op2);
      }
      pick.style.display=p>1?'':'none';
      info.textContent='共 '+total+' 行 · 显示 '+(from+1)+'–'+to+(size?(' · 每页 '+size+' 行'):' · 全部');
      prev.disabled=(page<=1); next.disabled=(page>=p);
    }
    prev.onclick=function(){page=page-1;render();};
    next.onclick=function(){page=page+1;render();};
    pick.onchange=function(){page=parseInt(pick.value,10)||1;render();};
    siz.onchange=function(){var v=parseInt(siz.value,10); size=v>0?v:0; page=1; render();};
    bar.appendChild(prev); bar.appendChild(pick); bar.appendChild(next); bar.appendChild(siz); bar.appendChild(info);
    var wrap=t.parentNode;
    wrap.parentNode.insertBefore(bar,wrap);
    render();
    return 1;
  }
  var tables=document.querySelectorAll('table'),used=0;
  for(var i=0;i<tables.length;i++) used+=initTable(tables[i]);
  document.documentElement.setAttribute('data-am-paginated',String(used));
})();
"""


def _hash(text: str) -> str:
    digest = hashlib.sha256(text.encode('utf-8')).digest()
    return "sha256-" + base64.b64encode(digest).decode('ascii')


def _csp_strip(html: str, hashes) -> str:
    for h in hashes:
        html = html.replace(f" '{h}'", "").replace(f"'{h}'", "")
    return html


def paginate_html(html: str, page_size: int = 50, min_rows: int = 26):
    """返回 (新 html, info)。幂等：重复调用只保留一份注入脚本与一个 hash。"""
    info = {'injected': False, 'removed_old': False, 'tables_eligible': 0, 'page_size': page_size}

    # 幂等：先清掉上一轮的脚本与它的 CSP hash
    m_old = re.search(re.escape(MARK) + r"(.*?)</script>", html, re.S)
    if m_old:
        old_script = m_old.group(1)
        html = html[:m_old.start()] + html[m_old.end():]
        html = _csp_strip(html, [_hash(old_script)])
        info['removed_old'] = True

    script = SCRIPT_TMPL.replace('__MIN_ROWS__', str(int(min_rows))).replace(
        '__PAGE_SIZE__', str(int(page_size)))

    # 只统计会被分页的表格（有 tbody 且行数达阈值），供调用方核对
    info['tables_eligible'] = len(re.findall(
        r'<tbody>(?:(?!</tbody>).)*?</tbody>', html, re.S))

    if '</body>' not in html:
        raise ValueError('page has no </body>')

    html = html.replace('</body>', MARK + script + '</script>\n</body>', 1)

    # CSP：把脚本 hash 追加进 script-src
    h = _hash(script)
    csp_re = re.compile(r'(<meta http-equiv="Content-Security-Policy" content="[^"]*?script-src )([^;"]*)')
    if csp_re.search(html):
        html = csp_re.sub(lambda m: m.group(1) + m.group(2).rstrip() + f" '{h}'", html, count=1)
    else:
        raise ValueError('no Content-Security-Policy meta with script-src')

    info['injected'] = True
    info['script_hash'] = h
    return html, info


def paginate_file(path, page_size: int = 50, min_rows: int = 26):
    p = Path(path)
    html = p.read_text(encoding='utf-8')
    new, info = paginate_html(html, page_size=page_size, min_rows=min_rows)
    if new != html:
        p.write_text(new, encoding='utf-8')
    return info


def main():
    ap = argparse.ArgumentParser(description='给 am 页面注入表格分页（幂等）')
    ap.add_argument('page')
    ap.add_argument('--page-size', type=int, default=50)
    ap.add_argument('--min-rows', type=int, default=26, help='行数低于此值的表格不分页')
    a = ap.parse_args()
    info = paginate_file(a.page, page_size=a.page_size, min_rows=a.min_rows)
    print(f"injected={info['injected']} removed_old={info['removed_old']} "
          f"tbody_blocks={info['tables_eligible']} page_size={info['page_size']}")
    print(f"hash={info.get('script_hash')}")
    return 0


if __name__ == '__main__':
    sys.exit(main())
