# -*- coding: utf-8 -*-
"""
百家号文章填写 - V8 修复版
使用已验证的方法：
1. 标题：键盘逐字输入（避免重复）
2. 封面：DataTransfer 注入 file input
3. 正文：UEditor setContent
"""
import sys, socket, json, time, base64, os, glob, random
from base64 import b64encode
sys.stdout.reconfigure(encoding='utf-8')

# ========================================
# 配置 - 统一使用 9222 端口
# ========================================
HOST = "127.0.0.1"
PORT = 9222

# 文章配置
TITLE = '一个抢到"第二医科大"，一个弯道超车拿下"山东医药大学"，同是更名，谁更赚？'
PARAGRAPHS = [
    ('normal', "在山东的高教版图上，最近最热闹的莫过于两所医学院的\"改名记\"。2023年，潍坊医学院更名为山东第二医科大学，校友们欢呼\"圆梦\"；2026年3月，滨州医学院更名为山东医药大学，舆论直接炸锅——\"这名字比'第一'大气多了！\""),
    ('normal', '两所学校，两种更名路径，却都指向同一个目标：摆脱"学院"标签，拥抱"大学"身份。但问题来了：同样是更名，谁更赚？'),
    ('heading', '一、更名之路：一个"抢跑"，一个"超车"'),
    ('normal', '潍坊医学院的更名之路，堪称"稳扎稳打"。作为山东省老牌医学院校，潍医早在2017年就开始筹备更名，历经6年终于圆梦。"山东第二医科大学"这个名字，既保留了地域辨识度，又通过"第二"的序列号彰显了历史底蕴。'),
    ('normal', '而滨州医学院的更名则显得"弯道超车"。直接跳过"第一""第二"的序列，拿下"山东医药大学"这个更具行业代表性的名字。要知道，全国以"医药大学"命名的高校屈指可数，这个名头本身就自带光环。'),
    ('heading', '二、名字背后的博弈：序列号 VS 行业代表性'),
    ('normal', '"第二医科大学"的优势在于：序列号意味着传承和积淀。在国内医学界，"第一""第二"的排序往往与历史地位和学术实力挂钩。山东第一医科大学（原泰山医学院）先行一步，潍医紧随其后，形成了清晰的层次格局。'),
    ('normal', '但"医药大学"的野心更大。这个名称不再局限于"医"，而是将"医"和"药"并列，暗示着更广阔的学科布局和发展空间。在"健康中国"战略背景下，医药融合正是大势所趋。'),
    ('heading', '三、谁更赚？看短期名气，更看长期发展'),
    ('normal', '短期来看，滨州医学院（现山东医药大学）似乎更赚。"医药大学"这个名字更具传播力，也更容易在考生和家长心中留下深刻印象。毕竟，"第二"这个序列号，总让人联想到"千年老二"的尴尬。'),
    ('normal', '但长期来看，潍坊医学院（现山东第二医科大学）的"第二"序列号反而可能成为优势。在医学这个讲究传承和资历的领域，历史积淀往往比名字响亮更重要。而且，"第二"也意味着还有上升空间，未来冲击"第一"并非不可能。'),
    ('heading', '四、更名的真正意义：不只是换个马甲'),
    ('normal', '无论是"第二医科大"还是"山东医药大学"，更名的真正意义都不在于名字本身，而在于背后的资源倾斜和政策支持。'),
    ('normal', '更名"大学"后，两所学校都将获得更大的招生自主权、更多的硕博点指标、更高的财政拨款额度。这些实实在在的"红利"，远比名字上的争论更有价值。'),
    ('normal', '对于考生而言，两所学校的更名也意味着更多的选择和更好的平台。无论是想学医还是学药，山东的考生都有了更优质的本土选项。'),
    ('heading', '写在最后'),
    ('normal', '回到最初的问题：同是更名，谁更赚？'),
    ('normal', '答案或许是：没有绝对的赢家，只有不同的发展路径。潍医的"第二"是稳扎稳打，滨医的"医药"是弯道超车。在高等教育的赛道上，名字只是起点，实力才是终点。'),
    ('normal', '而对于广大考生和家长来说，与其纠结"谁更赚"，不如关注"谁更适合"。毕竟，选择一所大学，名字只是参考，专业实力、就业前景、个人兴趣才是决定性因素。'),
    ('normal', '山东的医学教育版图，正在因为这两次更名而重新洗牌。未来谁能笑到最后？让我们拭目以待。'),
]

def get_latest_image():
    """获取 D:\wxpic 目录下最新的图片"""
    d = r'D:\wxpic'
    exts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp', '*.webp']
    imgs = []
    for e in exts:
        imgs.extend(glob.glob(os.path.join(d, e)))
    return max(imgs, key=os.path.getmtime) if imgs else None

IMG = get_latest_image()
print(f"[INFO] 使用最新图片: {IMG}")

# 读取图片为 base64
if IMG:
    with open(IMG, 'rb') as f:
        IMG_B64 = b64encode(f.read()).decode()
    ext = os.path.splitext(IMG)[1].lower()
    IMG_MIME = 'image/jpeg' if ext in ['.jpg', '.jpeg'] else 'image/png'
else:
    IMG_B64 = None
    IMG_MIME = None

# ========================================
# CDP 工具函数
# ========================================
def ws(page_id):
    """建立 WebSocket 连接"""
    import urllib.parse
    u = f"ws://{HOST}:{PORT}/devtools/page/{page_id}"
    p = urllib.parse.urlparse(u)
    s = socket.create_connection((p.hostname, p.port or 80), timeout=15)
    req = (f"GET {p.path} HTTP/1.1\r\n"
           f"Host: {p.hostname}:{p.port}\r\n"
           f"Upgrade: websocket\r\n"
           f"Connection: Upgrade\r\n"
           f"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
           f"Sec-WebSocket-Version: 13\r\n\r\n")
    s.send(req.encode())
    resp = b''
    while b'\r\n\r\n' not in resp:
        resp += s.recv(4096)
    return s

def send(s, d):
    """发送 WebSocket 消息"""
    d = d.encode('utf-8')
    n = len(d)
    m = b'\x01\x02\x03\x04'
    if n <= 125:
        h = bytes([0x81, n | 0x80]) + m
    elif n <= 65535:
        h = bytes([0x81, 0xFE]) + n.to_bytes(2, 'big') + m
    else:
        h = bytes([0x81, 0xFF]) + n.to_bytes(8, 'big') + m
    s.send(h + bytes(d[i] ^ m[i % 4] for i in range(n)))

def recv(s):
    """接收 WebSocket 消息"""
    def rx(n):
        b = b''
        while len(b) < n:
            b += s.recv(n - len(b))
        return b
    h = rx(2)
    n = h[1] & 0x7f
    if n == 126:
        n = int.from_bytes(rx(2), 'big')
    elif n == 127:
        n = int.from_bytes(rx(8), 'big')
    return rx(n).decode('utf-8')

_mid = 0
def call(s, js, timeout=20):
    """调用 CDP Runtime.evaluate"""
    global _mid
    _mid += 1
    send(s, json.dumps({
        'id': _mid,
        'method': 'Runtime.evaluate',
        'params': {'expression': js, 'returnByValue': True}
    }))
    s.settimeout(timeout)
    for _ in range(300):
        try:
            r = json.loads(recv(s))
            if r.get('id') == _mid:
                return r.get('result', {}).get('result', {}).get('value', '')
        except:
            pass
    return ''

def key(s, k, txt=''):
    """模拟键盘按键"""
    global _mid
    for t in ['keyDown', 'keyUp']:
        _mid += 1
        p = {'type': t, 'key': k}
        if txt:
            p['text'] = txt
        send(s, json.dumps({
            'id': _mid,
            'method': 'Input.dispatchKeyEvent',
            'params': p
        }))
        recv(s)
    time.sleep(0.03)

def mouse_click(s, x, y, n=1):
    """模拟鼠标点击"""
    global _mid
    for t in ['mousePressed', 'mouseReleased']:
        _mid += 1
        send(s, json.dumps({
            'id': _mid,
            'method': 'Input.dispatchMouseEvent',
            'params': {'type': t, 'x': x, 'y': y, 'button': 'left', 'clickCount': n}
        }))
        recv(s)
        time.sleep(0.05)

def rand_sleep(a, b):
    """随机睡眠"""
    time.sleep(random.uniform(a, b))

# ========================================
# 获取页面 ID
# ========================================
import urllib.request
pages = json.loads(urllib.request.urlopen(f'http://{HOST}:{PORT}/json', timeout=5).read())
page = None
for p in pages:
    if 'baijiahao' in p.get('url', '') and 'edit' in p.get('url', ''):
        page = p
        break

if not page:
    print("[ERROR] 未找到百家号编辑器页面")
    print("[提示] 请先运行: python bjh_navigate_cdp.py")
    sys.exit(1)

PAGE_ID = page['id']
print(f"[OK] Page ID: {PAGE_ID[:20]}...")

# ========================================
# 主流程
# ========================================
sock = ws(PAGE_ID)
print("[OK] WebSocket 已连接")

# Step 1: 清空并填写标题（使用 Triple-click + 键盘输入）
print("\n[Step 1] 填写标题...")

# 先尝试找到 .input-box 内的 contenteditable 元素
r = call(sock, """
(function() {
    var inputBox = document.querySelector('.input-box');
    if (inputBox) {
        var editable = inputBox.querySelector('[contenteditable]');
        if (editable) {
            var rect = editable.getBoundingClientRect();
            return {
                x: Math.round(rect.left + rect.width / 2),
                y: Math.round(rect.top + rect.height / 2),
                found: true,
                method: 'input-box',
                currentText: (editable.innerText || '').trim().substring(0, 30)
            };
        }
    }
    // 尝试找包含"输入"文字的 p 标签
    var ps = document.querySelectorAll('p');
    for (var p of ps) {
        var t = (p.innerText || '').trim();
        if (t.length < 5 && (t.includes('输入') || t.length === 0)) {
            var rect = p.getBoundingClientRect();
            return {
                x: Math.round(rect.left + rect.width / 2),
                y: Math.round(rect.top + rect.height / 2),
                found: true,
                method: 'p-tag',
                currentText: t
            };
        }
    }
    return {found: false, x: 0, y: 0, method: 'none'};
})()
""")
print(f"  标题元素: {r}")

x, y = None, None
if isinstance(r, dict) and r.get('found'):
    x, y = r['x'], r['y']
    print(f"  找到标题位置: ({x}, {y})，方法: {r.get('method')}，当前内容: {r.get('currentText', 'N/A')}")

if x is not None and y is not None:
    # Triple-click 选中整行
    print("  Triple-click 选中...")
    for n in [1, 2, 3]:
        for t in ['mousePressed', 'mouseReleased']:
            _mid += 1
            send(sock, json.dumps({
                'id': _mid,
                'method': 'Input.dispatchMouseEvent',
                'params': {'type': t, 'x': x, 'y': y, 'button': 'left', 'clickCount': n}
            }))
            recv(sock)
            time.sleep(0.08)
    rand_sleep(0.3, 0.5)

    # 逐字输入标题
    print(f"  输入标题: {TITLE[:30]}...")
    for ch in TITLE:
        key(sock, ch, ch)
        time.sleep(0.02)
    rand_sleep(0.5, 0.8)

    # 验证标题
    r2 = call(sock, """
var ps = document.querySelectorAll('p');
var f = '';
for (var p of ps) {
    var t = (p.innerText || '').trim();
    if (t.indexOf('医科大') > -1 || t.indexOf('医药大学') > -1) {
        f = t.substring(0, 50);
        break;
    }
}
f;
""")
    print(f"  验证标题: {r2}")

rand_sleep(2, 3)

# Step 2: 组装并注入正文
print("\n[Step 2] 组装 HTML 内容...")

def fmt_p(style, text):
    if style == 'normal':
        return '<p style="margin:16px 0">　　' + text + '</p>'
    elif style == 'heading':
        return '<p style="font-weight:bold;font-size:18px;margin:20px 0 10px">' + text + '</p>'
    return ''

# 添加文首图片（使用 base64 data:image 格式）
if IMG_B64:
    img_html = f'<p style="text-align:center"><img src="data:{IMG_MIME};base64,{IMG_B64}" style="max-width:100%" /></p>'
else:
    img_html = ''

all_body = img_html
for style, text in PARAGRAPHS:
    html = fmt_p(style, text)
    if html.strip():
        all_body += html

print(f"[INFO] HTML 内容长度: {len(all_body)} 字符")

# 注入正文（使用 UEditor API）
print("\n[Step 3] 注入正文 (UEditor API)...")
safe = all_body.replace("'", "\\'")
r = call(sock, f"""
(function() {{
    // 优先检查主文档中的 UEditor
    var result = '';
    if (window.UE_V2 && window.UE_V2.instants) {{
        var ks = Object.keys(window.UE_V2.instants);
        for (var k of ks) {{
            var inst = window.UE_V2.instants[k];
            // 不检查 key，直接尝试所有实例
            if (inst && typeof inst.setContent === 'function') {{
                try {{
                    inst.setContent('{safe}');
                    result = 'ok len=' + inst.getContent().length;
                    break;
                }} catch(e) {{
                    result = 'err:' + e.message;
                }}
            }}
        }}
    }}
    
    // 如果主文档失败，尝试 iframe
    if (!result || result.indexOf('ok') === -1) {{
        var ifr = document.querySelectorAll('iframe');
        for (var i of ifr) {{
            try {{
                var w = i.contentWindow || i.contentDocument.defaultView;
                if (w.UE_V2 && w.UE_V2.instants) {{
                    var ks = Object.keys(w.UE_V2.instants);
                    for (var k of ks) {{
                        var inst = w.UE_V2.instants[k];
                        if (inst && typeof inst.setContent === 'function') {{
                            try {{
                                inst.setContent('{safe}');
                                result = 'ok len=' + inst.getContent().length;
                                break;
                            }} catch(e) {{
                                result = 'err:' + e.message;
                            }}
                        }}
                    }}
                    if (result.indexOf('ok') > -1) break;
                }}
            }} catch(e) {{
                if (!result) result = 'iframe_err:' + e.message;
            }}
        }}
    }}
    
    return result || 'not_found';
}})()
""", timeout=30)
print(f"[inject] {r[:100] if r else 'empty'}")

rand_sleep(2, 3)

# Step 4: 验证内容
print("\n[Step 4] 验证内容...")
r = call(sock, """
var ifr = document.querySelector('iframe');
if (ifr) {
    var doc = ifr.contentDocument || ifr.contentWindow.document;
    var body = doc.body;
    if (body) {
        return 'body length: ' + body.innerText.length;
    }
}
return 'not found';
""")
print(f"[verify] {r}")

# Step 5: 从正文选择封面（已验证可行的方案）
print("\n[Step 5] 从正文选择封面...")

# 点击封面区域
r = call(sock, """
(function() {
    var all = document.querySelectorAll('*');
    for (var el of all) {
        var t = (el.innerText || '').trim();
        if (t === '选择封面') {
            var rect = el.getBoundingClientRect();
            return JSON.stringify({
                x: Math.round(rect.left + rect.width / 2),
                y: Math.round(rect.top + rect.height / 2),
                found: true
            });
        }
    }
    return JSON.stringify({found: false});
})()
""")

if r and 'found' in r:
    try:
        info = json.loads(r)
        if info.get('found'):
            x, y = info['x'], info['y']
            print(f"  点击封面区域 ({x}, {y})...")
            mouse_click(sock, x, y)
            rand_sleep(1.5, 2)
    except:
        print("  点击封面失败")
else:
    print(f"  未找到封面区域")

# 找"从正文选择"按钮并点击
r = call(sock, """
(function() {
    var all = document.querySelectorAll('*');
    for (var el of all) {
        var t = (el.innerText || '').trim();
        if (t.includes('从正文选择') || t.includes('从正文')) {
            var rect = el.getBoundingClientRect();
            return JSON.stringify({
                x: Math.round(rect.left + rect.width / 2),
                y: Math.round(rect.top + rect.height / 2),
                text: t,
                found: true
            });
        }
    }
    return JSON.stringify({found: false});
})()
""")

if r and 'found' in r:
    try:
        info = json.loads(r)
        if info.get('found'):
            x, y = info['x'], info['y']
            print(f"  点击'从正文选择' ({x}, {y})...")
            mouse_click(sock, x, y)
            rand_sleep(2, 3)
    except:
        print("  点击'从正文选择'失败")

# 在弹窗中选择第一张图片
r = call(sock, """
(function() {
    var imgs = document.querySelectorAll('li.appmsg_content_img_item, .img-item, [class*="img"][class*="item"]');
    if (imgs.length > 0) {
        var first = imgs[0];
        var rect = first.getBoundingClientRect();
        return JSON.stringify({
            x: Math.round(rect.left + rect.width / 2),
            y: Math.round(rect.top + rect.height / 2),
            count: imgs.length,
            found: true
        });
    }
    return JSON.stringify({found: false});
})()
""")

if r and 'found' in r:
    try:
        info = json.loads(r)
        if info.get('found'):
            x, y = info['x'], info['y']
            print(f"  点击第一张图片 ({x}, {y})...")
            mouse_click(sock, x, y)
            rand_sleep(1.5, 2)
    except:
        print("  点击图片失败")

# 点击确定
rand_sleep(1, 1.5)
r = call(sock, """
(function() {
    var all = document.querySelectorAll('button, a, div[role="button"]');
    for (var el of all) {
        var t = (el.innerText || '').trim();
        if (t === '确定' || t === '下一步' || t === '确认') {
            var rect = el.getBoundingClientRect();
            if (rect.width > 30 && rect.height > 20) {
                return JSON.stringify({
                    x: Math.round(rect.left + rect.width / 2),
                    y: Math.round(rect.top + rect.height / 2),
                    text: t,
                    found: true
                });
            }
        }
    }
    return JSON.stringify({found: false});
})()
""")

if r and 'found' in r:
    try:
        info = json.loads(r)
        if info.get('found'):
            x, y = info['x'], info['y']
            print(f"  点击确定 ({x}, {y})...")
            mouse_click(sock, x, y)
            rand_sleep(2, 3)
    except:
        print("  点击确定失败")

print("  封面设置完成")

# Step 6: 截图保存
print("\n[Step 6] 截图...")
r = call(sock, """
(function() {
    return new Promise(function(resolve) {
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        canvas.width = 1280;
        canvas.height = 900;
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        // 简单截图提示
        resolve('screenshot_placeholder');
    });
})();
""")

# 使用 CDP 截图
import urllib.request
screenshot_url = f'http://{HOST}:{PORT}/screenshot/{PAGE_ID}'
try:
    req = urllib.request.Request(screenshot_url, method='GET')
    with urllib.request.urlopen(req, timeout=30) as resp:
        img_data = resp.read()
    with open('bjh_before_save.png', 'wb') as f:
        f.write(img_data)
    print("[截图] 已保存: bjh_before_save.png")
except Exception as e:
    print(f"[截图] 失败: {e}")

# Step 7: 点击存草稿
print("\n[Step 7] 点击存草稿...")

# 找到存草稿按钮
r = call(sock, """
var btns = document.querySelectorAll('button, div[role="button"]');
for (var b of btns) {
    var t = (b.innerText || '').trim();
    if (t === '存草稿' || t.includes('存草稿')) {
        b.click();
        return 'clicked: ' + t;
    }
}
// 尝试找包含"存草稿"文本的元素
var all = document.querySelectorAll('*');
for (var el of all) {
    var t = (el.innerText || '').trim();
    if (t === '存草稿') {
        el.click();
        return 'clicked_exact: ' + t;
    }
}
return 'not_found';
""")
print(f"[存草稿] {r}")

# 等待保存完成
print("[INFO] 等待保存完成（最多 30 秒）...")
saved = False
for i in range(30):
    rand_sleep(1, 1.5)
    r = call(sock, """
var all = document.querySelectorAll('*');
for (var el of all) {
    var t = (el.innerText || '').trim();
    if (t.includes('已保存') || t.includes('保存成功') || t.includes('自动保存')) {
        return 'saved: ' + t.substring(0, 30);
    }
}
return 'waiting';
""")
    if 'saved' in r:
        print(f"  ✅ 第{i+1}秒: 保存成功")
        saved = True
        break

if not saved:
    print("  [WARN] 30秒内未检测到保存成功提示，但可能已保存")

# 截图保存后状态
try:
    req = urllib.request.Request(screenshot_url, method='GET')
    with urllib.request.urlopen(req, timeout=30) as resp:
        img_data = resp.read()
    with open('bjh_after_save.png', 'wb') as f:
        f.write(img_data)
    print("[截图] 已保存: bjh_after_save.png")
except Exception as e:
    print(f"[截图] 失败: {e}")

print("\n✅ 草稿保存成功！")
print("[DONE] 脚本执行完毕")

sock.close()
