#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
同步脚本 — 将本地所有增量内容同步到 5.60 生产服务器
包括：
  1. 后端文件（playwright_executor.py / smart_locate_service.py / execution_service.py / scheduled_tasks.py）
  2. page_url_mapping.json（25 页）
  3. 用例数据（本地有服务器没有的 18 条 + 更新已有）
  4. 重启容器

用法:
    cd backend && python scripts/sync_to_560_full.py
"""
import json
import os
import sys
import time
from pathlib import Path

sys.stdout.reconfigure(encoding='utf-8')
os.chdir(Path(__file__).parent.parent)  # 切到 backend/

import paramiko
import pymysql

SERVER = ('192.168.5.60', 'ubains', 'Ubains@123')
MYSQL_PORT = 3307
MYSQL_USER = 'platapp'
MYSQL_PWD = 'PlatApp2026'
MYSQL_DB = 'plat_auto_test'

REMOTE_BASE = '/data/third_party/plat-auto-test'
REMOTE_BACKEND = f'{REMOTE_BASE}/backend'

# 需要同步的后端文件（本地路径 → 远程相对路径）
BACKEND_FILES = {
    'app/executors/playwright_executor.py': 'app/executors/playwright_executor.py',
    'app/services/smart_locate_service.py': 'app/services/smart_locate_service.py',
    'app/services/execution_service.py': 'app/services/execution_service.py',
    'app/routers/scheduled_tasks.py': 'app/routers/scheduled_tasks.py',
    'app/data/page_url_mapping.json': 'app/data/page_url_mapping.json',
}

INSERT_SQL = (
    "INSERT INTO test_cases "
    "(id, module_id, name, description, status, priority, tags, steps, config, "
    "depends_on, parameters, `condition`, `order`, version, created_at, updated_at, case_type) "
    "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) "
    "ON DUPLICATE KEY UPDATE "
    "name=VALUES(name), description=VALUES(description), priority=VALUES(priority), "
    "tags=VALUES(tags), steps=VALUES(steps), config=VALUES(config), "
    "depends_on=VALUES(depends_on), parameters=VALUES(parameters), "
    "`condition`=VALUES(`condition`), `order`=VALUES(`order`), version=VALUES(version), "
    "updated_at=VALUES(updated_at), case_type=VALUES(case_type)"
)


def load_local_cases():
    """从本地 SQLite 加载所有用例"""
    import sqlite3
    conn = sqlite3.connect('data/test_platform.db')
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute("SELECT * FROM test_cases ORDER BY module_id, name")
    rows = [dict(r) for r in cur.fetchall()]
    conn.close()
    return rows


def load_local_mapping():
    with open('app/data/page_url_mapping.json', encoding='utf-8') as f:
        return json.load(f)


def sync_backend_files():
    """通过 SFTP 上传后端文件到服务器"""
    cli = paramiko.SSHClient()
    cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    cli.connect(SERVER[0], username=SERVER[1], password=SERVER[2], timeout=10)
    sftp = cli.open_sftp()
    results = {}
    try:
        for local_rel, remote_rel in BACKEND_FILES.items():
            local_path = local_rel
            remote_path = f'{REMOTE_BACKEND}/{remote_rel}'
            try:
                sftp.put(local_path, remote_path)
                size = os.path.getsize(local_path)
                results[local_rel] = f'OK ({size}B)'
            except Exception as e:
                results[local_rel] = f'FAIL: {e}'
    finally:
        sftp.close()
        cli.close()
    return results


def get_remote_case_ids():
    """获取服务器 MySQL 中所有用例 ID"""
    conn = pymysql.connect(host=SERVER[0], port=MYSQL_PORT, user=MYSQL_USER,
                           password=MYSQL_PWD, database=MYSQL_DB,
                           charset='utf8mb4', connect_timeout=8)
    try:
        cur = conn.cursor()
        cur.execute("SELECT id, module_id, name FROM test_cases")
        return {r[0]: {'module_id': r[1], 'name': r[2]} for r in cur.fetchall()}
    finally:
        conn.close()


def sync_cases(local_cases, remote_ids):
    """批量 UPSERT 用例到服务器 MySQL"""
    conn = pymysql.connect(host=SERVER[0], port=MYSQL_PORT, user=MYSQL_USER,
                           password=MYSQL_PWD, database=MYSQL_DB,
                           charset='utf8mb4', connect_timeout=8)
    try:
        cur = conn.cursor()
        inserted = updated = skipped = 0
        for c in local_cases:
            cid = c['id']
            if cid in remote_ids:
                # 检查是否有变化（简单判断 updated_at 不同则更新）
                # 由于 MySQL datetime 精度可能不同，直接 UPSERT
                cur.execute(INSERT_SQL, (
                    cid, c['module_id'], c['name'], c.get('description', ''),
                    c['status'], c['priority'], c['tags'], c['steps'], c['config'],
                    c['depends_on'], c['parameters'], c['condition'],
                    c['order'], c['version'], c['created_at'], c['updated_at'],
                    c['case_type'],
                ))
                if cur.rowcount == 1:
                    inserted += 1
                else:
                    updated += 1
            else:
                cur.execute(INSERT_SQL, (
                    cid, c['module_id'], c['name'], c.get('description', ''),
                    c['status'], c['priority'], c['tags'], c['steps'], c['config'],
                    c['depends_on'], c['parameters'], c['condition'],
                    c['order'], c['version'], c['created_at'], c['updated_at'],
                    c['case_type'],
                ))
                inserted += 1

        conn.commit()
        return inserted, updated
    finally:
        conn.close()


def restart_container():
    """重启 5.60 容器"""
    cli = paramiko.SSHClient()
    cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    cli.connect(SERVER[0], username=SERVER[1], password=SERVER[2], timeout=10)
    try:
        stdin, stdout, stderr = cli.exec_command(
            'cd /data/third_party/plat-auto-test/deploy && docker compose restart app',
            timeout=60
        )
        out = stdout.read().decode('utf-8', 'replace')
        err = stderr.read().decode('utf-8', 'replace')
        return out.strip(), err.strip()
    finally:
        cli.close()


def wait_healthy(retries=12, interval=5):
    """等待容器健康"""
    import urllib.request
    for i in range(retries):
        try:
            resp = urllib.request.urlopen('http://192.168.5.60/health', timeout=5)
            if resp.status == 200:
                return True, resp.read().decode('utf-8')
        except Exception:
            pass
        print(f'  等待容器启动... ({i+1}/{retries})')
        time.sleep(interval)
    return False, ''


def main():
    print('===== 1. 同步后端文件 =====')
    file_results = sync_backend_files()
    for f, r in file_results.items():
        print(f'  {f}: {r}')

    print('\n===== 2. 同步用例数据 =====')
    local_cases = load_local_cases()
    remote_ids = get_remote_case_ids()
    print(f'  本地: {len(local_cases)} 条, 服务器: {len(remote_ids)} 条')

    # 统计差异
    only_local = [c for c in local_cases if c['id'] not in remote_ids]
    common_ids = [c['id'] for c in local_cases if c['id'] in remote_ids]
    print(f'  本地新增: {len(only_local)} 条, 共同: {len(common_ids)} 条')

    inserted, updated = sync_cases(local_cases, remote_ids)
    print(f'  同步结果: 新增 {inserted} 条, 更新 {updated} 条')

    # 验证
    remote_ids_after = get_remote_case_ids()
    print(f'  同步后服务器: {len(remote_ids_after)} 条')

    # 按模块统计
    from collections import Counter
    module_cnt = Counter()
    for c in local_cases:
        if c['id'] not in remote_ids:
            module_cnt[c['module_id']] += 1
    if module_cnt:
        print(f'  新增用例按模块分布:')
        for mid, cnt in module_cnt.most_common():
            print(f'    {mid}: {cnt} 条')

    print('\n===== 3. 重启容器 =====')
    out, err = restart_container()
    print(f'  docker compose restart 输出: {out or "(空)"}')
    if err:
        print(f'  stderr: {err}')

    print('\n===== 4. 等待健康检查 =====')
    ok, body = wait_healthy()
    if ok:
        print(f'  ✅ 容器健康: {body[:100]}')
    else:
        print(f'  ❌ 容器未在预期时间内恢复健康')

    print('\n===== 同步完成 =====')


if __name__ == '__main__':
    main()