#!/usr/bin/env python3
"""
easyclaw 可视化仪表盘生成器
===========================
读取 easyclaw 日志文件，生成一个自包含的 HTML 可视化仪表盘。

用法：
  python easyclaw_dashboard.py <日志文件路径> [--output dashboard.html] [--since YYYY-MM-DD]
"""

import argparse
import json
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta

LOG_PATTERN = re.compile(
    r"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.*)$"
)
BYTES_PATTERN = re.compile(r"(\d+)\s*bytes")
URL_PATTERN = re.compile(r"https?://\S+")


def parse_log_file(path, since=None):
    entries = []
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            m = LOG_PATTERN.match(line)
            if not m:
                continue
            ts_str, level, message = m.groups()
            try:
                ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
            except ValueError:
                continue
            if since and ts < since:
                continue
            entries.append({"timestamp": ts, "level": level.upper(), "message": message, "raw": line})
    return entries


def build_dashboard_data(entries, log_path):
    if not entries:
        return {"empty": True}

    level_counts = Counter(e["level"] for e in entries)
    total_tasks = level_counts.get("SUCCESS", 0) + level_counts.get("ERROR", 0)
    success_rate = round(level_counts["SUCCESS"] / total_tasks * 100, 1) if total_tasks > 0 else 0

    total_bytes = 0
    url_counts = Counter()
    hourly = defaultdict(lambda: {"total": 0, "success": 0, "error": 0})
    daily = defaultdict(lambda: {"total": 0, "success": 0, "error": 0, "bytes": 0})
    errors = []
    recent_logs = []

    for e in entries:
        bm = BYTES_PATTERN.search(e["message"])
        if bm:
            total_bytes += int(bm.group(1))
        um = URL_PATTERN.search(e["message"])
        if um:
            url_counts[um.group()] += 1

        hour_key = e["timestamp"].strftime("%Y-%m-%d %H:00")
        day_key = e["timestamp"].strftime("%Y-%m-%d")
        hourly[hour_key]["total"] += 1
        daily[day_key]["total"] += 1

        if e["level"] == "SUCCESS":
            hourly[hour_key]["success"] += 1
            daily[day_key]["success"] += 1
            if bm:
                daily[day_key]["bytes"] += int(bm.group(1))
        elif e["level"] == "ERROR":
            hourly[hour_key]["error"] += 1
            daily[day_key]["error"] += 1
            errors.append({"ts": e["timestamp"].strftime("%Y-%m-%d %H:%M:%S"), "msg": e["message"]})

    for e in entries[-200:]:
        lc = {"INFO": "info", "SUCCESS": "success", "ERROR": "error", "WARNING": "warning"}.get(e["level"], "info")
        recent_logs.append({
            "ts": e["timestamp"].strftime("%Y-%m-%d %H:%M:%S"),
            "level": e["level"],
            "lc": lc,
            "msg": e["message"],
        })

    first_ts = entries[0]["timestamp"]
    last_ts = entries[-1]["timestamp"]
    age_seconds = (datetime.now() - last_ts).total_seconds()
    if age_seconds < 300:
        status = "running"
    elif age_seconds < 3600:
        status = "idle"
    else:
        status = "offline"

    # hourly sorted
    sorted_hourly = sorted(hourly.items())
    # daily sorted
    sorted_daily = sorted(daily.items())

    # top URLs
    top_urls = url_counts.most_common(15)

    def fmt_bytes(n):
        for unit in ("B", "KB", "MB", "GB"):
            if n < 1024:
                return f"{n:.1f} {unit}"
            n /= 1024
        return f"{n:.1f} TB"

    return {
        "empty": False,
        "log_path": os.path.basename(log_path),
        "total_entries": len(entries),
        "first_ts": first_ts.strftime("%Y-%m-%d %H:%M:%S"),
        "last_ts": last_ts.strftime("%Y-%m-%d %H:%M:%S"),
        "status": status,
        "level_counts": dict(level_counts),
        "success_rate": success_rate,
        "total_tasks": total_tasks,
        "total_bytes": total_bytes,
        "total_bytes_fmt": fmt_bytes(total_bytes),
        "unique_urls": len(url_counts),
        "hourly_labels": [h[0] for h in sorted_hourly],
        "hourly_success": [h[1]["success"] for h in sorted_hourly],
        "hourly_error": [h[1]["error"] for h in sorted_hourly],
        "hourly_total": [h[1]["total"] for h in sorted_hourly],
        "daily_labels": [d[0] for d in sorted_daily],
        "daily_success": [d[1]["success"] for d in sorted_daily],
        "daily_error": [d[1]["error"] for d in sorted_daily],
        "daily_bytes": [d[1]["bytes"] for d in sorted_daily],
        "top_urls": [{"url": u, "count": c} for u, c in top_urls],
        "errors": errors[-50:],
        "recent_logs": recent_logs,
        "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }
HTML_TEMPLATE = r'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>easyclaw Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
*{box-sizing:border-box}
body{font-family:'Inter',system-ui,sans-serif;background:#0b0f1a;color:#e2e8f0;margin:0}
.mono{font-family:'JetBrains Mono',monospace}
.glass{background:rgba(15,23,42,0.7);backdrop-filter:blur(12px);border:1px solid rgba(99,102,241,0.12)}
.card{border-radius:16px;padding:24px;transition:all .2s}
.card:hover{border-color:rgba(99,102,241,0.3);box-shadow:0 0 30px rgba(99,102,241,0.06)}
.status-dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:8px}
.status-running{background:#22c55e;box-shadow:0 0 8px #22c55e}
.status-idle{background:#eab308;box-shadow:0 0 8px #eab308}
.status-offline{background:#ef4444;box-shadow:0 0 8px #ef4444}
.log-line{padding:3px 12px;border-radius:4px;font-size:12.5px;line-height:1.7}
.log-line:hover{background:rgba(99,102,241,0.08)}
.log-success{border-left:3px solid #22c55e}
.log-error{border-left:3px solid #ef4444}
.log-warning{border-left:3px solid #eab308}
.log-info{border-left:3px solid #6366f1}
::-webkit-scrollbar{width:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(99,102,241,0.3);border-radius:3px}
.chart-container{width:100%;height:320px}
.glow-text{background:linear-gradient(135deg,#818cf8,#6366f1,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.stat-value{font-size:2rem;font-weight:700;line-height:1.2}
.tab-btn{padding:8px 20px;border-radius:8px;cursor:pointer;transition:all .15s;font-size:13px;font-weight:500;border:1px solid transparent}
.tab-btn.active{background:rgba(99,102,241,0.2);border-color:rgba(99,102,241,0.4);color:#a5b4fc}
.tab-btn:not(.active){color:#64748b}
.tab-btn:not(.active):hover{color:#94a3b8;background:rgba(99,102,241,0.06)}
.url-bar{height:6px;border-radius:3px;background:linear-gradient(90deg,#6366f1,#818cf8);transition:width .6s ease}
</style>
</head>
<body class="min-h-screen">
<div class="max-w-[1400px] mx-auto px-6 py-8">
  <!-- Header -->
  <div class="flex items-center justify-between mb-8">
    <div>
      <h1 class="text-2xl font-bold tracking-tight"><span class="glow-text">easyclaw</span> <span class="text-slate-400 font-normal">Monitor</span></h1>
      <p class="text-slate-500 text-sm mt-1 mono">__LOG_PATH__ &middot; Generated __GENERATED_AT__</p>
    </div>
    <div class="flex items-center gap-3">
      <span class="status-dot status-__STATUS__"></span>
      <span class="text-sm font-medium" id="statusText"></span>
    </div>
  </div>

  <!-- KPI Cards -->
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
    <div class="glass card">
      <div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Total Entries</div>
      <div class="stat-value text-indigo-400 mono" id="kpiTotal"></div>
    </div>
    <div class="glass card">
      <div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Success Rate</div>
      <div class="stat-value mono" id="kpiRate"></div>
    </div>
    <div class="glass card">
      <div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Data Fetched</div>
      <div class="stat-value text-cyan-400 mono" id="kpiBytes"></div>
    </div>
    <div class="glass card">
      <div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Unique URLs</div>
      <div class="stat-value text-violet-400 mono" id="kpiUrls"></div>
    </div>
  </div>

  <!-- Charts Row 1 -->
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
    <div class="glass card lg:col-span-2">
      <div class="flex items-center justify-between mb-4">
        <h2 class="text-sm font-semibold text-slate-300">Hourly Activity</h2>
        <div class="flex gap-1" id="hourlyTabs">
          <div class="tab-btn active" data-range="24">24h</div>
          <div class="tab-btn" data-range="72">3d</div>
          <div class="tab-btn" data-range="all">All</div>
        </div>
      </div>
      <div class="chart-container" id="chartHourly"></div>
    </div>
    <div class="glass card">
      <h2 class="text-sm font-semibold text-slate-300 mb-4">Level Distribution</h2>
      <div class="chart-container" id="chartPie"></div>
    </div>
  </div>

  <!-- Charts Row 2 -->
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
    <div class="glass card">
      <h2 class="text-sm font-semibold text-slate-300 mb-4">Daily Data Volume</h2>
      <div class="chart-container" id="chartDaily"></div>
    </div>
    <div class="glass card">
      <h2 class="text-sm font-semibold text-slate-300 mb-4">Top URLs</h2>
      <div id="urlList" class="space-y-3 overflow-y-auto" style="max-height:310px"></div>
    </div>
  </div>

  <!-- Errors & Logs -->
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
    <div class="glass card">
      <h2 class="text-sm font-semibold text-red-400 mb-4">Recent Errors <span class="text-slate-500 font-normal" id="errorCount"></span></h2>
      <div id="errorList" class="space-y-1 overflow-y-auto mono" style="max-height:360px;font-size:12px"></div>
    </div>
    <div class="glass card">
      <div class="flex items-center justify-between mb-4">
        <h2 class="text-sm font-semibold text-slate-300">Live Logs</h2>
        <div class="flex gap-1" id="logTabs">
          <div class="tab-btn active" data-filter="all">All</div>
          <div class="tab-btn" data-filter="success">Success</div>
          <div class="tab-btn" data-filter="error">Error</div>
          <div class="tab-btn" data-filter="warning">Warn</div>
        </div>
      </div>
      <div id="logList" class="space-y-0.5 overflow-y-auto mono" style="max-height:360px"></div>
    </div>
  </div>

  <div class="text-center text-slate-600 text-xs py-4">easyclaw monitor &middot; dashboard auto-generated</div>
</div>

<script>
const D = __DATA_JSON__;

// --- KPIs ---
document.getElementById('kpiTotal').textContent = D.total_entries.toLocaleString();
const rate = D.success_rate;
const rateEl = document.getElementById('kpiRate');
rateEl.textContent = rate + '%';
rateEl.className = 'stat-value mono ' + (rate >= 90 ? 'text-emerald-400' : rate >= 70 ? 'text-yellow-400' : 'text-red-400');
document.getElementById('kpiBytes').textContent = D.total_bytes_fmt;
document.getElementById('kpiUrls').textContent = D.unique_urls;

const statusMap = {running:'Running', idle:'Idle', offline:'Offline'};
document.getElementById('statusText').textContent = statusMap[D.status] || D.status;

// --- ECharts Theme ---
const baseOpt = {
  grid:{left:50,right:20,top:30,bottom:30},
  tooltip:{backgroundColor:'rgba(15,23,42,0.95)',borderColor:'rgba(99,102,241,0.3)',textStyle:{color:'#e2e8f0',fontSize:12}},
};

// --- Hourly Chart ---
const hourlyChart = echarts.init(document.getElementById('chartHourly'));
function renderHourly(range) {
  let labels = D.hourly_labels, s = D.hourly_success, e = D.hourly_error;
  if (range !== 'all') {
    const n = parseInt(range);
    labels = labels.slice(-n); s = s.slice(-n); e = e.slice(-n);
  }
  hourlyChart.setOption({
    ...baseOpt,
    xAxis:{type:'category',data:labels,axisLabel:{color:'#64748b',fontSize:10,rotate:45},axisLine:{lineStyle:{color:'#1e293b'}}},
    yAxis:{type:'value',splitLine:{lineStyle:{color:'#1e293b'}},axisLabel:{color:'#64748b',fontSize:10}},
    series:[
      {name:'Success',type:'bar',stack:'a',data:s,itemStyle:{color:'#22c55e',borderRadius:[0,0,0,0]},barMaxWidth:12},
      {name:'Error',type:'bar',stack:'a',data:e,itemStyle:{color:'#ef4444',borderRadius:[2,2,0,0]},barMaxWidth:12},
    ],
    legend:{show:true,top:0,right:0,textStyle:{color:'#94a3b8',fontSize:11}},
    tooltip:{trigger:'axis'},
  });
}
renderHourly('24');
document.querySelectorAll('#hourlyTabs .tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('#hourlyTabs .tab-btn').forEach(b=>b.classList.remove('active'));
    btn.classList.add('active');
    renderHourly(btn.dataset.range);
  });
});

// --- Pie Chart ---
const pieChart = echarts.init(document.getElementById('chartPie'));
const lc = D.level_counts;
const pieColors = {SUCCESS:'#22c55e',ERROR:'#ef4444',WARNING:'#eab308',INFO:'#6366f1'};
pieChart.setOption({
  tooltip:{backgroundColor:'rgba(15,23,42,0.95)',borderColor:'rgba(99,102,241,0.3)',textStyle:{color:'#e2e8f0'}},
  series:[{
    type:'pie',radius:['45%','72%'],center:['50%','55%'],
    label:{color:'#94a3b8',fontSize:11},
    data:Object.entries(lc).map(([k,v])=>({name:k,value:v,itemStyle:{color:pieColors[k]||'#475569'}})),
    emphasis:{itemStyle:{shadowBlur:20,shadowColor:'rgba(99,102,241,0.4)'}},
  }],
});

// --- Daily Chart ---
const dailyChart = echarts.init(document.getElementById('chartDaily'));
dailyChart.setOption({
  ...baseOpt,
  xAxis:{type:'category',data:D.daily_labels,axisLabel:{color:'#64748b',fontSize:10},axisLine:{lineStyle:{color:'#1e293b'}}},
  yAxis:[
    {type:'value',name:'Tasks',splitLine:{lineStyle:{color:'#1e293b'}},axisLabel:{color:'#64748b',fontSize:10},nameTextStyle:{color:'#64748b'}},
    {type:'value',name:'Bytes',splitLine:{show:false},axisLabel:{color:'#64748b',fontSize:10,formatter:v=>(v/1024/1024).toFixed(1)+'MB'},nameTextStyle:{color:'#64748b'}},
  ],
  series:[
    {name:'Success',type:'bar',data:D.daily_success,itemStyle:{color:'rgba(34,197,94,0.7)',borderRadius:[4,4,0,0]},barMaxWidth:28},
    {name:'Error',type:'bar',data:D.daily_error,itemStyle:{color:'rgba(239,68,68,0.7)',borderRadius:[4,4,0,0]},barMaxWidth:28},
    {name:'Data',type:'line',yAxisIndex:1,data:D.daily_bytes,smooth:true,lineStyle:{color:'#06b6d4',width:2},itemStyle:{color:'#06b6d4'},areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(6,182,212,0.2)'},{offset:1,color:'rgba(6,182,212,0)'}])}},
  ],
  legend:{show:true,top:0,right:0,textStyle:{color:'#94a3b8',fontSize:11}},
  tooltip:{trigger:'axis'},
});

// --- URL List ---
const urlMax = D.top_urls.length ? D.top_urls[0].count : 1;
document.getElementById('urlList').innerHTML = D.top_urls.map(u => `
  <div class="flex items-center gap-3">
    <div class="flex-1 min-w-0">
      <div class="text-xs text-slate-400 truncate">${u.url}</div>
      <div class="mt-1 bg-slate-800 rounded-full overflow-hidden"><div class="url-bar" style="width:${u.count/urlMax*100}%"></div></div>
    </div>
    <div class="text-xs text-indigo-400 mono font-semibold w-12 text-right">${u.count}</div>
  </div>
`).join('');

// --- Errors ---
document.getElementById('errorCount').textContent = `(${D.errors.length})`;
document.getElementById('errorList').innerHTML = D.errors.slice().reverse().map(e => `
  <div class="log-line log-error"><span class="text-slate-500">${e.ts}</span> <span class="text-red-400">${e.msg}</span></div>
`).join('');

// --- Logs ---
let allLogs = D.recent_logs;
function renderLogs(filter) {
  const logs = filter === 'all' ? allLogs : allLogs.filter(l => l.lc === filter);
  document.getElementById('logList').innerHTML = logs.slice().reverse().map(l => `
    <div class="log-line log-${l.lc}"><span class="text-slate-500">${l.ts}</span> <span class="text-${l.lc==='success'?'emerald':l.lc==='error'?'red':l.lc==='warning'?'yellow':'indigo'}-400">[${l.level}]</span> ${l.msg}</div>
  `).join('');
}
renderLogs('all');
document.querySelectorAll('#logTabs .tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('#logTabs .tab-btn').forEach(b=>b.classList.remove('active'));
    btn.classList.add('active');
    renderLogs(btn.dataset.filter);
  });
});

// --- Resize ---
window.addEventListener('resize', () => {
  hourlyChart.resize(); pieChart.resize(); dailyChart.resize();
});
</script>
</body>
</html>'''
def generate_html(data: dict) -> str:
    """将数据嵌入 HTML 模板。"""
    html = HTML_TEMPLATE
    html = html.replace("__DATA_JSON__", json.dumps(data, ensure_ascii=False))
    html = html.replace("__LOG_PATH__", data.get("log_path", ""))
    html = html.replace("__GENERATED_AT__", data.get("generated_at", ""))
    html = html.replace("__STATUS__", data.get("status", "offline"))
    return html


def main():
    parser = argparse.ArgumentParser(description="easyclaw 可视化仪表盘生成器")
    parser.add_argument("logfile", help="easyclaw 日志文件路径")
    parser.add_argument("--output", "-o", default="easyclaw_dashboard.html", help="输出 HTML 文件路径 (默认 easyclaw_dashboard.html)")
    parser.add_argument("--since", help="只分析此时间之后的日志 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)")

    args = parser.parse_args()

    since = None
    if args.since:
        for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
            try:
                since = datetime.strptime(args.since, fmt)
                break
            except ValueError:
                continue
        if since is None:
            print("错误: --since 格式不正确")
            sys.exit(1)

    if not os.path.isfile(args.logfile):
        print(f"错误: 找不到日志文件 '{args.logfile}'")
        sys.exit(1)

    print(f"正在解析日志: {args.logfile} ...")
    entries = parse_log_file(args.logfile, since=since)
    if not entries:
        print("日志为空或无匹配记录。")
        sys.exit(1)

    print(f"解析到 {len(entries)} 条记录，正在生成仪表盘 ...")
    data = build_dashboard_data(entries, args.logfile)
    html = generate_html(data)

    with open(args.output, "w", encoding="utf-8") as f:
        f.write(html)

    print(f"仪表盘已生成: {args.output}")


if __name__ == "__main__":
    main()
