#!/usr/bin/env python3
"""Run U45 MyBatis-Plus on the existing loopback MySQL; record only synthetic observations."""
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import tempfile
import uuid
from datetime import datetime

ROOT = Path(__file__).resolve().parent
EVIDENCE = ROOT / 'evidence/u45-2026-10-31.json'
SOURCES = ['pom.xml', 'README.md', 'run.py',
           'src/main/java/study/u45/U45Lab.java', 'src/main/java/study/u45/U45Cleanup.java',
           'src/main/java/study/u45/Item.java', 'src/main/java/study/u45/ItemMapper.java']
REQUIRED_JARS = ['mybatis-plus-core-3.5.18.1.jar', 'mybatis-plus-extension-3.5.18.jar',
                 'mybatis-plus-spring-3.5.18.1.jar', 'mybatis-plus-jsqlparser-3.5.18.jar',
                 'mybatis-spring-4.1.0.jar', 'mysql-connector-j-9.7.0.jar']
MARKERS = ['schema-created=', 'mysql=8.4.7', 'target=PASS', 'crud=PASS', 'wrapper=PASS',
           'page=PASS', 'tenant=PASS', 'nativesql=PASS', 'optimistic=PASS', 'blockattack=PASS',
           'order=PASS', 'cleanup=PASS']


def diagnostics(output):
    if isinstance(output, bytes):
        output = output.decode('utf-8', 'replace')
    output = output or ''
    for secret in (os.environ.get('U45_MYSQL_PASSWORD', ''), os.environ.get('U45_MYSQL_USER', '')):
        if secret:
            output = output.replace(secret, '<REDACTED>')
    lines = [line for line in output.splitlines() if any(x in line for x in ('Exception', 'ERROR', 'Caused by:', 'error', '拒绝'))]
    return [re.sub(r'(?i)(password|pwd)=([^&\s]+)', r'\1=<REDACTED>', line)[:500] for line in lines[-6:]]


def main():
    java_home = Path(os.environ.get('JAVA_HOME', '/Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home'))
    java = java_home / 'bin/java'
    mvn = shutil.which('mvn')
    if not java.is_file() or not mvn:
        raise RuntimeError('需要 JDK 21 和 Maven 3.9，且 MySQL 由 DBngin 运行于 127.0.0.1:3306')
    allowed = ('PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'TZ', 'U45_MYSQL_USER', 'U45_MYSQL_PASSWORD')
    env = {key: os.environ[key] for key in allowed if key in os.environ}
    env.update(JAVA_HOME=str(java_home), MAVEN_OPTS='-Xmx384m -XX:ActiveProcessorCount=2')
    java_version = subprocess.check_output([str(java), '-version'], stderr=subprocess.STDOUT, text=True, env=env).strip()
    if '21.0.11' not in java_version:
        raise RuntimeError('实验基线需要 JDK 21.0.11')
    result = {'issue': 82, 'input': 'python3 labs/mybatis-plus-u45/run.py',
              'startedAt': datetime.now().astimezone().isoformat(),
              'environment': {'os': platform.platform(), 'arch': platform.machine()}, 'status': 'failed', 'exitCode': 1,
              'observations': []}
    schema = 'study_u45_' + uuid.uuid4().hex[:12]
    result['schema'] = schema
    try:
        with tempfile.TemporaryDirectory(prefix='mybatis-plus-u45-') as directory:
            cp = Path(directory) / 'classpath.txt'
            cmd = [mvn, '-o', '-B', '-q', 'compile', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath',
                   '-DincludeScope=runtime', f'-Dmdep.outputFile={cp}']
            subprocess.run(cmd, cwd=ROOT, env=env, check=True, timeout=180, capture_output=True, text=True)
            classpath = str(ROOT / 'target/classes') + os.pathsep + cp.read_text().strip()
            versions = [x for x in cp.read_text().split(os.pathsep)
                        if any(k in x for k in ['/mybatis-plus-core/', '/mybatis-plus-extension/', '/mybatis-plus-spring/', '/mybatis-plus-jsqlparser/', '/mybatis-spring/', '/mysql-connector-j/'])
                        and '/spring-boot-native' not in x]
            result['dependencies'] = [Path(x).name for x in versions]
            for required in REQUIRED_JARS:
                if not any(required in d for d in result['dependencies']):
                    raise RuntimeError('依赖版本不符: ' + required + '; actual=' + str(result['dependencies']))
            command = [str(java), '-Dspring.config.location=optional:file:/dev/null', '-Xms64m', '-Xmx256m',
                       '-XX:ActiveProcessorCount=2', '-Du45.trace=true', '-cp', classpath, 'study.u45.U45Lab', schema]
            returncode = None
            result['phase'] = 'java-lab'
            stderr = ''
            try:
                run = subprocess.run(command, cwd=ROOT, env=env, capture_output=True, text=True, timeout=120)
                stdout, stderr, returncode = run.stdout, run.stderr, run.returncode
            except subprocess.TimeoutExpired as exc:
                stdout = (exc.stdout or b'').decode('utf-8', 'replace') if isinstance(exc.stdout, bytes) else (exc.stdout or '')
                stderr = exc.stderr or ''
            result['observations'] = [line for line in stdout.splitlines() if line.startswith('U45 ')]
            created = 'U45 schema-created=' + schema in result['observations']
            cleaned = 'U45 cleanup=PASS own-schema=removed' in result['observations']
            if created and not cleaned:
                rescue = subprocess.run([str(java), '-Xmx64m', '-cp', classpath, 'study.u45.U45Cleanup', schema],
                                        cwd=ROOT, env=env, capture_output=True, text=True, timeout=30)
                if rescue.returncode != 0:
                    result['diagnostics'] = diagnostics(rescue.stderr + rescue.stdout)
                    raise RuntimeError('本次 schema 应急清理失败，请检查 ' + schema)
                result['observations'].append('U45 emergency-cleanup=PASS own-schema=removed')
            if returncode != 0 or not all(any(line.startswith('U45 ' + m) for line in result['observations']) for m in MARKERS):
                result['diagnostics'] = diagnostics(stderr) + diagnostics(stdout)
                raise RuntimeError('实验进程失败或观察缺失；exit=' + str(returncode)
                                   + ('；本次 schema 已清理' if cleaned or created else '；未观察到创建标记，请核对 schema ' + schema))
            result.update(status='passed', exitCode=0, java=java_version,
                          limits='MySQL 8.4.7 单机；手动装配 SqlSessionFactory，未启用 Boot 自动配置；未验证多数据源、DataPermission 插件、自定义 SQL 注入器与生产容量。',
                          artifactSha256={p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in SOURCES})
    except Exception as exc:
        if isinstance(exc, subprocess.CalledProcessError):
            result['phase'] = 'maven'
            result['diagnostics'] = diagnostics(exc.stderr) + diagnostics(exc.stdout)
        result['error'] = type(exc).__name__ + ': ' + str(exc)
        raise
    finally:
        if result['status'] == 'passed':
            EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
            tmp = EVIDENCE.with_suffix('.tmp')
            tmp.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
            tmp.replace(EVIDENCE)
        else:
            print(json.dumps(result, ensure_ascii=False, indent=2))
    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
