#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CloudBase 双入口适配：看板页 API 走绝对地址(Tailscale Tunnel) + 双入口 iframe 相对路径 + 后端精确 CORS。
全部用 str.find(str) 精确锚点替换，对大文件不做正则。"""
import io, sys, os

W = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DEMO = os.path.join(W, '工具/仪表盘/三部门逐月拆分_demo.html')
IDX = os.path.join(W, '工具/index.html')
APP = os.path.join(W, '工具/app.html')
SRV = os.path.join(W, '脚本/销售工作台服务.py')

fails = []

def edit(path, pairs, label):
    s = open(path, encoding='utf-8').read()
    for old, new, cnt in pairs:
        n = s.count(old)
        if n != cnt:
            fails.append('%s: 锚点命中 %d 次(期望 %d): %r' % (label, n, cnt, old[:70]))
            continue
        s = s.replace(old, new)
        print('  [%s] ✓ %s' % (label, old[:56].replace('\n', '\\n')))
    if fails and any(f.startswith(label) for f in fails):
        return False
    open(path, 'w', encoding='utf-8').write(s)
    print('  [%s] 已写回 %d bytes' % (label, len(s.encode('utf-8'))))
    return True

# ───────────────────────── 1. 看板页 API 基址 ─────────────────────────
DEMO_OLD_BASE = """function _sandeApiBase(){
  try{ if(/^https?:/.test(window.location.protocol)) return ''; }catch(e){}
  return null;
}"""

DEMO_NEW_BASE = """/* ===== 后端基址（2026-10-06 双入口适配）=====
   本地 127.0.0.1:8765 / Tailscale 直连：后端与页面同源 → 相对路径直调 /api/sandept
   CloudBase（纯静态托管、无后端）：与工作台其它 Tab 一致，走公网 Tunnel 绝对地址
   不硬编码任何密钥：token 仍由工作台登录态经 localStorage(bior_auth_token) 或 URL ?t= 提供 */
var SANDE_API_ABS = ['https://api.biorisales.site:8443', 'https://api.biorisales.site'];
function _sandeApiBases(){
  var list = [];
  try{
    var proto = window.location.protocol;
    if(proto !== 'http:' && proto !== 'https:') return list;
    var h = String(window.location.hostname || '').toLowerCase();
    if(h === '127.0.0.1' || h === 'localhost' || /\\.ts\\.net$/.test(h)){ list.push(''); }
    else { for(var i = 0; i < SANDE_API_ABS.length; i++) list.push(SANDE_API_ABS[i]); }
  }catch(e){}
  return list;
}
function _sandeApiBase(){
  var l = _sandeApiBases();
  return l.length ? l[0] : null;
}"""

DEMO_OLD_BOOT_START = "function _sandeBoot(){"
DEMO_NEW_BOOT = """function _sandeBoot(){
  var bases = _sandeApiBases();
  if(!bases.length){ _sandeDenied('\u8bf7\u901a\u8fc7\u5de5\u4f5c\u53f0\uff08\u767b\u5f55\u6001\uff09\u8bbf\u95ee\u672c\u9875'); return; }
  var token = _sandeToken();
  if(!token){ _sandeDenied('\u672a\u767b\u5f55\u6216\u65e0\u6743\u8bbf\u95ee'); return; }
  var _i = 0;
  function _attempt(){
    if(_i >= bases.length){ _sandeDenied('\u6570\u636e\u52a0\u8f7d\u5931\u8d25\uff08\u7f51\u7edc\u5f02\u5e38\uff09'); return; }
    var base = bases[_i++];
    var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
    var timer = ctrl ? setTimeout(function(){ try{ ctrl.abort(); }catch(e){} }, 15000) : null;
    var opts = { method:'GET', cache:'no-store', headers:{ 'Authorization':'Bearer ' + token } };
    if(ctrl) opts.signal = ctrl.signal;
    return fetch(base + '/api/sandept', opts)
      .then(function(r){
        if(timer) clearTimeout(timer);
        if(r.status === 401 || r.status === 403){ _sandeDenied('\u672a\u767b\u5f55\u6216\u65e0\u6743\u8bbf\u95ee'); return; }
        if(!r.ok){ return _attempt(); }
        return r.json().then(function(j){
          if(!j || !j.ok || !j.data){ return _attempt(); }
          if(!Array.isArray(j.data.months) || !j.data.months.length){ _sandeDenied('\u6570\u636e\u4e3a\u7a7a'); return; }
          _sandeApply(j.data);
        });
      })
      .catch(function(){ if(timer) clearTimeout(timer); return _attempt(); });
  }
  _attempt();
}"""

# ───────────────────────── 2. 双入口 iframe 相对路径 ─────────────────────────
HELPER = """/* 看板 iframe 地址：按当前页面所在目录推导，本地 /工具/ 与 CloudBase /workbench/ 共用同一份相对路径 */
function sandeptFrameSrc() {
  try {
    var dir = String(window.location.pathname || '/').replace(/[^/]*$/, '');
    if (!dir) dir = '/';
    return dir + '仪表盘/三部门逐月拆分_demo.html';
  } catch (e) { return '/工具/仪表盘/三部门逐月拆分_demo.html'; }
}
function renderSandept(main) {"""

IFRAME_OLD = """      '<iframe id="sandeptFrame" src="/工具/仪表盘/三部门逐月拆分_demo.html?v=20261006a' + sandeptUserQS() + '" ' +"""
IFRAME_NEW = """      '<iframe id="sandeptFrame" src="' + sandeptFrameSrc() + '?v=20261006b' + sandeptUserQS() + '" ' +"""

# ───────────────────────── 3. 后端精确 CORS ─────────────────────────
SRV_OLD_CFG = """AUTH_FILE = os.path.join(DATA_DIR, 'auth_sessions.json')
TOKEN_TTL = 7 * 86400  # 7 天滑动续期"""

SRV_NEW_CFG = """AUTH_FILE = os.path.join(DATA_DIR, 'auth_sessions.json')
TOKEN_TTL = 7 * 86400  # 7 天滑动续期

# ── CORS（2026-10-06）：精确来源白名单 ────────────────────────────────────────
# 为什么不是 '*'：/api/sandept 等接口靠 Authorization: Bearer 凭据鉴权，
#   「ACAO=* + 凭据」既是反模式、也会被未来开启 credentials 的调用方拒绝。
#   这里只回显白名单来源（CloudBase 静态托管域 / Tunnel 域 / Tailscale 域 / 本机），
#   无 Origin 的同源或命令行请求不注入 ACAO。Function Call 与工作台其它 Tab 不受影响。
CORS_ORIGIN_HOSTS = ('api.biorisales.site',)


def cors_allow_origin(origin):
    \"\"\"返回允许回显的 ACAO 值；不在白名单 / 非法来源 → None（不注入头）。\"\"\"
    o = (origin or '').strip()
    if not o:
        return None
    try:
        from urllib.parse import urlparse
        p = urlparse(o)
    except Exception:
        return None
    if p.scheme not in ('http', 'https'):
        return None
    host = (p.hostname or '').lower()
    if not host:
        return None
    if host in ('127.0.0.1', 'localhost') or host in CORS_ORIGIN_HOSTS:
        return o
    if host.endswith('.tcloudbaseapp.com') or host.endswith('.ts.net'):
        return o
    return None"""

SRV_OLD_HOOK = """    def _send_json(self, obj, code=200):"""
SRV_NEW_HOOK = """    def _cors_headers(self):
        \"\"\"注入精确来源 CORS 头（白名单外不注入；绝不与凭据共用 '*'）。\"\"\"
        o = cors_allow_origin(self.headers.get('Origin'))
        if o:
            self.send_header('Access-Control-Allow-Origin', o)
            self.send_header('Access-Control-Allow-Credentials', 'true')
            self.send_header('Vary', 'Origin')

    def _send_json(self, obj, code=200):"""

SRV_OLD_ACAO = "self.send_header('Access-Control-Allow-Origin', '*')"
SRV_NEW_ACAO = "self._cors_headers()"

print('== 1. 看板页 API 基址 ==')
edit(DEMO, [(DEMO_OLD_BASE, DEMO_NEW_BASE, 1)], 'DEMO')

# _sandeBoot 用「函数头 → 下一个行首 }」定位，避免正则
s = open(DEMO, encoding='utf-8').read()
i = s.find(DEMO_OLD_BOOT_START)
if i < 0:
    fails.append('DEMO: 找不到 _sandeBoot')
else:
    j = s.find('\n}\n', i)
    if j < 0:
        fails.append('DEMO: 找不到 _sandeBoot 结束')
    else:
        s = s[:i] + DEMO_NEW_BOOT + s[j + 2:]
        open(DEMO, 'w', encoding='utf-8').write(s)
        print('  [DEMO] ✓ _sandeBoot 重写（多基址回退）')
        print('  [DEMO] 已写回 %d bytes' % len(s.encode('utf-8')))

print('== 2. 双入口 iframe ==')
for p, lb in ((IDX, 'INDEX'), (APP, 'APP')):
    edit(p, [(IFRAME_OLD, IFRAME_NEW, 1),
             ('function renderSandept(main) {', HELPER, 1)], lb)

print('== 3. 后端 CORS ==')
edit(SRV, [(SRV_OLD_CFG, SRV_NEW_CFG, 1),
           (SRV_OLD_HOOK, SRV_NEW_HOOK, 1),
           (SRV_OLD_ACAO, SRV_NEW_ACAO, 4)], 'SERVER')

print()
if fails:
    print('❌ 失败：')
    for f in fails:
        print('   - ' + f)
    sys.exit(1)
print('✅ 全部替换完成')
