#!/usr/bin/env python3
"""
AKASHA 智能管理中控台 - 完整后端
运行: python3 app.py  (端口 9999)
"""

import warnings
warnings.filterwarnings("ignore", message=".*NotOpenSSLWarning.*")
warnings.filterwarnings("ignore", message=".*urllib3 v2.*")
import os, sys, json, glob, time
import requests
import threading
from flask import Flask, jsonify, render_template, request, redirect
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from nlp import parse
from task_engine import list_tasks, get_task, create_task, update_task, delete_task, execute_task

app = Flask(__name__)

STATE_FILE = "/opt/akasha-system/state/ozon_skus.json"
SHOPS_DIR = "/opt/akasha-system/state/shops"

def _read_json(path):
    if not os.path.isfile(path): return None
    try:
        with open(path,"r",encoding="utf-8") as f: return json.load(f)
    except: return None


def _to_num(val, default=0.0):
    """Convert comma-decimal strings to float: '4,5' -> 4.5"""
    if val is None:
        return default
    if isinstance(val, (int, float)):
        return float(val)
    try:
        return float(str(val).replace(",", "."))
    except (ValueError, TypeError):
        return default

def _normalize_products(prods):
    """Normalize all numeric fields in a product list."""
    for p in prods:
        for key in ("health_score", "price", "old_price", "stock", "images_count"):
            if key in p:
                p[key] = _to_num(p[key], 0 if key in ("stock", "images_count") else 0.0)
                if key in ("stock", "images_count"):
                    p[key] = int(p[key])
    return prods

def _shop_names():
    if not os.path.isdir(SHOPS_DIR): return []
    return sorted([os.path.splitext(f)[0] for f in os.listdir(SHOPS_DIR) if f.endswith(".json")])


# ── 钉钉告警配置 ──
WEBHOOK_URL = "https://oapi.dingtalk.com/robot/send?access_token=12831e6130cddc3f3325f10cb9afc91d72f178266ec3787b560ac78fc1f09af8"
ALERT_STATE_FILE = "/opt/akasha-system/state/alert_config.json"
ALERT_TRACK_FILE = "/opt/akasha-system/state/alert_state.json"

def send_dingtalk_alert(title, text):
    """Send Markdown alert to DingTalk group."""
    if not WEBHOOK_URL:
        return False
    body = {
        "msgtype": "markdown",
        "markdown": {"title": title, "text": text},
    }
    try:
        r = requests.post(WEBHOOK_URL, json=body, timeout=10)
        return r.json().get("errcode") == 0
    except Exception as e:
        print("[ALERT] send failed:", e, flush=True)
        return False

def get_alert_enabled():
    """Return whether auto-alert is enabled."""
    try:
        if os.path.isfile(ALERT_STATE_FILE):
            with open(ALERT_STATE_FILE) as f:
                return json.load(f).get("enabled", True)
    except: pass
    return True  # default on

def set_alert_enabled(val):
    """Persist alert toggle state."""
    os.makedirs(os.path.dirname(ALERT_STATE_FILE), exist_ok=True)
    with open(ALERT_STATE_FILE, "w") as f:
        json.dump({"enabled": bool(val)}, f)


def _load_alert(pid):
    """Load alert state for a product."""
    try:
        if os.path.isfile(ALERT_TRACK_FILE):
            with open(ALERT_TRACK_FILE) as f:
                d = json.load(f)
                return d.get("products", {}).get(str(pid), {})
    except: pass
    return {}

def _save_alert_state(state):
    """Persist full alert state file."""
    os.makedirs(os.path.dirname(ALERT_TRACK_FILE), exist_ok=True)
    with open(ALERT_TRACK_FILE, "w") as f:
        json.dump(state, f, indent=2, ensure_ascii=False)

def _read_alert_state():
    """Read full alert state, return dict with 'products' and 'history'."""
    try:
        if os.path.isfile(ALERT_TRACK_FILE):
            with open(ALERT_TRACK_FILE) as f:
                return json.load(f)
    except: pass
    return {"products": {}, "history": []}

def _alert_msg(level, name, pid, score, old_score, count):
    """Build alert message for a given level."""
    if level == 1:
        return ("[AKASHA] {name}: health score low").format(name=name[:20]), (
            "## \u26a0\ufe0f \u5546\u54c1\u5065\u5eb7\u5206\u504f\u4f4e\n\n"
            "**\u5546\u54c1**: {name}\n"
            "**ID**: {pid}\n"
            "**\u5f53\u524d\u5206\u6570**: {score}\n"
            "\u8bf7\u5173\u6ce8\u3002"
        ).format(name=name, pid=pid, score=score)
    elif level == 2:
        return ("[AKASHA] {name}: health score declining").format(name=name[:20]), (
            "## \ud83d\udd34 \u5546\u54c1\u5065\u5eb7\u5206\u6301\u7eed\u6076\u5316\n\n"
            "**\u5546\u54c1**: {name}\n"
            "**ID**: {pid}\n"
            "**\u4e0a\u6b21\u5206\u6570**: {old}\n"
            "**\u5f53\u524d\u5206\u6570**: {score}\n"
            "\u8bf7\u7acb\u5373\u5904\u7406\u3002"
        ).format(name=name, pid=pid, old=old_score, score=score)
    else:
        return ("[AKASHA] {name}: alert limit reached - remove suggested").format(name=name[:20]), (
            "## \ud83d\udea8 \u5546\u54c1\u5df2\u8fbe 3 \u6b21\u544a\u8b66\u4e0a\u9650\n\n"
            "**\u5546\u54c1**: {name}\n"
            "**ID**: {pid}\n"
            "**\u5f53\u524d\u5206\u6570**: {score}\n"
            "**\u7d2f\u8ba1\u544a\u8b66**: {count} \u6b21\n\n"
            "\u5efa\u8bae\u5f3a\u5236\u4e0b\u67b6\u3002"
        ).format(name=name, pid=pid, score=score, count=count)


def check_low_health_alert():
    if not get_alert_enabled():
        return []
    data = _read_json("/opt/akasha-system/state/ozon_skus.json")
    if not data:
        return []
    prods = _normalize_products(data.get("products", []))
    state = _read_alert_state()
    results = []
    now = __import__("time").strftime("%Y-%m-%dT%H:%M:%S")
    for p in prods:
        pid = p.get("product_id")
        score = _to_num(p.get("health_score"), 100)
        name = p.get("name", "?")
        offer = p.get("offer_id", "?")
        if score < 45 and score > 0:
            ps = _load_alert(pid) if pid else {}
            old = ps.get("last_score", 0.0)
            cnt = ps.get("alert_count", 0)
            lvl = 2 if (cnt > 0 and old > 0 and score < old) else 1
            cnt += 1
            if cnt >= 3:
                lvl = 3
            title, msg = _alert_msg(lvl, name, pid, score, old, cnt)
            ok = send_dingtalk_alert(title, msg)
            results.append({"product_id": pid, "score": score, "level": lvl, "sent": ok})
            state["products"][str(pid)] = {
                "alert_level": lvl, "alert_count": cnt,
                "last_score": score, "offer_id": offer,
                "last_alerted": now,
            }
            state.setdefault("history", []).append({
                "time": now, "product_id": pid, "offer_id": offer,
                "name": name, "level": lvl, "score": score,
                "message": title,
            })
        elif pid and score > _load_alert(pid).get("last_score", 0):
            ps2 = _load_alert(pid)
            if ps2.get("alert_count", 0) > 0:
                ps2["alert_count"] = 0
                ps2["alert_level"] = 0
                ps2["last_score"] = score
                state["products"][str(pid)] = ps2
    state["history"] = state.get("history", [])[-200:]
    _save_alert_state(state)
    print("[ALERT] checked", len(prods), "got", len(results), flush=True)
    return results
@app.route("/")
def home():    return redirect("/control")
@app.route("/control")
def api_control():
    return render_template("control.html")

@app.errorhandler(404)
def _404(e):   return render_template("index.html"), 200

# ─── API: 商品 ───

@app.route("/api/products")
def api_products():
    shop = request.args.get("shop","")
    limit = int(request.args.get("limit","200"))
    data = _read_json(STATE_FILE)
    if not data: return jsonify({"total":0,"products":[]})
    prods = _normalize_products(data.get("products",[]))
    total = data.get("total",0)
    check_low_health_alert()
    return jsonify({"total":total,"shown":min(len(prods),limit),"products":prods[:limit]})

@app.route("/api/products/stats")
def api_stats():
    data = _read_json(STATE_FILE)
    if not data:
        return jsonify({"total":0,"high":0,"medium":0,"low":0,"in_stock":0,"out_of_stock":0,"sync_time":"","shops":len(_shop_names())})
    prods = _normalize_products(data.get("products",[]))
    hs = data.get("health_summary",{})
    ins = sum(1 for p in prods if p.get("stock",0)>0)
    outs = len(prods) - ins
    return jsonify({
        "total":data.get("total",0),
        "high":hs.get("high",0),
        "medium":hs.get("medium",0),
        "low":hs.get("low",0),
        "in_stock":ins,
        "out_of_stock":outs,
        "sync_time":data.get("sync_time",""),
        "shops":len(_shop_names()),
    })

@app.route("/api/products/<int:pid>")
def api_product(pid):
    for s in _shop_names():
        d = _read_json(os.path.join(SHOPS_DIR, s+".json"))
        if d:
            for p in d.get("products",[]):
                if p.get("product_id")==pid:
                    p["shop"]=s; return jsonify(p)
    data = _read_json(STATE_FILE)
    if data:
        for p in data.get("products",[]):
            if p.get("product_id")==pid: return jsonify(p)
    return jsonify({"error":"not found"}),404

# ─── API: 店铺 ───

@app.route("/api/shops")
def api_shops():
    return jsonify([{"name":n} for n in _shop_names()])

# ─── API: 任务 ───

@app.route("/api/tasks")
def api_tasks():
    return jsonify(list_tasks())

@app.route("/api/tasks/<int:tid>")
def api_task(tid):
    t = get_task(tid)
    return jsonify(t) if t else (jsonify({"error":"not found"}),404)

@app.route("/api/tasks/<int:tid>", methods=["DELETE"])
def api_del_task(tid):
    return jsonify({"success":delete_task(tid)})

@app.route("/api/tasks/<int:tid>/execute", methods=["POST"])
def api_exec(tid):
    return jsonify(execute_task(tid))

# ─── API: 指令 ───

@app.route("/api/command", methods=["POST"])
def api_cmd():
    data = request.get_json(force=True)
    text = data.get("text","")
    if not text: return jsonify({"error":"empty"}),400
    parsed = parse(text)
    parsed["response"] = _exec_intent(parsed["intent"], parsed["entities"])
    return jsonify(parsed)

def _exec_intent(intent, entities):
    """执行指令，返回可读结果。"""
    if intent == "query_product":
        pid = entities.get("product_id")
        oid = entities.get("offer_id")
        target = None
        for s in _shop_names():
            d = _read_json(os.path.join(SHOPS_DIR,s+".json"))
            if d:
                for p in d.get("products",[]):
                    if pid and p.get("product_id")==pid: target=p; target["shop"]=s; break
                    if oid and p.get("offer_id","").lower()==oid.lower() and not target: target=p; target["shop"]=s
            if target: break
        if not target:
            d = _read_json(STATE_FILE)
            if d:
                for p in d.get("products",[]):
                    if pid and p.get("product_id")==pid: target=p; break
        if target:
            return "商品: {name}\nID: {product_id} | Offer: {offer_id}\n价格: {price} {currency_code}\n库存: {stock}\n健康分: {health_score}/{health_level}\n店铺: {shop}".format(**target)
        return "未找到该商品。"

    elif intent == "check_stock":
        out = []
        for s in _shop_names():
            d = _read_json(os.path.join(SHOPS_DIR,s+".json"))
            if d:
                for p in d.get("products",[]):
                    if p.get("stock",0)==0 and p.get("state")=="active":
                        p["shop"]=s; out.append(p)
        if out:
            lines = ["缺货商品 ({n} 个):".format(n=len(out))]
            for p in out[:10]:
                lines.append("  [{shop}] {name} (ID: {product_id}) 健康分: {health_score}".format(**p))
            if len(out)>10: lines.append("  ... 还有 {n} 个".format(n=len(out)-10))
            return "\n".join(lines)
        return "所有商品均有库存。"

    elif intent == "show_stats":
        d = _read_json(STATE_FILE)
        if d:
            hs = d.get("health_summary",{})
            return "Ozon 数据概览\n商品总数: {total}\n健康 - 高: {high} / 中: {medium} / 低: {low}\n最后同步: {sync_time}".format(total=d.get("total",0),high=hs.get("high",0),medium=hs.get("medium",0),low=hs.get("low",0),sync_time=d.get("sync_time","未知"))
        return "暂无数据。"

    elif intent == "check_health":
        d = _read_json(STATE_FILE)
        if d:
            mins = entities.get("min_score",50)
            low = [p for p in _normalize_products(d.get("products",[])) if _to_num(p.get("health_score"),100) < mins]
            if low:
                lines = ["健康分低于 {m} 的商品 ({n} 个):".format(m=mins,n=len(low))]
                for p in low[:10]:
                    lines.append("  {name} (score={score})".format(name=p.get("name","?"),score=p.get("health_score","?")))
                if len(low)>10: lines.append("  ... 还有 {n} 个".format(n=len(low)-10))
                return "\n".join(lines)
            return "没有健康分低于 {m} 的商品。".format(m=mins)
        return "暂无数据。"

    elif intent == "run_task":
        tid = entities.get("task_id")
        if tid:
            r = execute_task(tid)
            return "任务 #{tid} 执行{result}。".format(tid=tid,result="完成" if r.get("success") else "失败")
        return "未指定任务编号。"

    elif intent == "list_tasks":
        tasks = list_tasks()
        lines = ["任务列表 ({n})".format(n=len(tasks))]
        for t in tasks:
            lines.append("  #{id} {name} [{status}]".format(**t))
        return "\n".join(lines)

    elif intent == "sync_ozon":
        return "Ozon 同步指令已下发，请稍后查看结果。"

    elif intent == "unknown":
        return "无法理解该指令。试试: 查商品 5092971729 / 缺货商品 / 统计商品 / 启动任务#1 / 同步Ozon"

    return "指令已执行: " + intent


# ─── API: 告警 ───

@app.route("/api/test_alert")
def api_test_alert():
    """Send a test alert to verify DingTalk config."""
    ok = send_dingtalk_alert(
        "[AKASHA] \u6d4b\u8bd5\u544a\u8b66",
        "## \u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u544a\u8b66\n\n\u5982\u679c\u4f60\u770b\u5230\u8fd9\u6761\u6d88\u606f\uff0c\u8bf4\u660e\u7cfb\u7edf\u544a\u8b66\u529f\u80fd\u6b63\u5e38\u5de5\u4f5c\u3002"
    )
    return jsonify({"success": ok, "message": "test alert sent" if ok else "send failed"})

@app.route("/api/alert_enabled")
def api_alert_config_get():
    """Get or set alert enabled state. GET: read. POST: toggle."""
    if request.method == "POST":
        data = request.get_json(force=True)
        val = data.get("enabled", True)
        set_alert_enabled(val)
        return jsonify({"enabled": bool(val)})
    return jsonify({"enabled": get_alert_enabled()})

@app.route("/api/alert_enabled", methods=["POST"])
def api_alert_enabled_post():
    data = request.get_json(force=True)
    val = data.get("enabled", True)
    set_alert_enabled(val)
    return jsonify({"enabled": bool(val)})

@app.route("/api/check_alerts")
def api_check_alerts():
    """Manually trigger low-health scan. Returns newly alerted products."""
    result = check_low_health_alert()
    return jsonify({"alerts_sent": len(result), "details": result})


@app.route("/api/alert_history")
def api_alert_history():
    """Return last 50 alert history entries."""
    state = _read_alert_state()
    return jsonify(state.get("history", [])[-50:])

@app.route("/api/alert_status")
def api_product_alert_status():
    """Return current alert levels for all products that have alerts."""
    state = _read_alert_state()


# ── Module 2: 阿米巴财报查询 ──

@app.route("/api/amiba/query")
def api_amiba_query():
    data_path = "/opt/akasha-system/data/amiba_report.json"
    if not os.path.isfile(data_path):
        return jsonify({"error": "amiba data not found"}), 404
    try:
        with open(data_path) as f:
            data = json.load(f)
    except Exception as e:
        return jsonify({"error": str(e)}), 500
    unit = request.args.get("unit_name", "")
    if unit:
        for u in data.get("units", []):
            if u.get("unit_name", "") == unit:
                return jsonify(u)
        return jsonify({"error": "unit not found: " + unit}), 404
    return jsonify(data)


# ── Module 3: 资产看板实时状态 API ──

@app.route("/api/dashboard-status")
def api_dashboard_status():
    now = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat()
    result = {
        "survey_id": "AKASHA-LIVE-" + __import__("time").strftime("%Y%m%d-%H%M%S"),
        "timestamp": now,
        "auditor": "auto_scanner",
        "nodes": {}, "summary": {},
        "frp_tunnel": {"status": "unknown"},
        "cc_switch": {"status": "unknown"},
    }
    assets = []

    # 1) Judge HTTP checks
    jf = "/opt/akasha-system/judge/last_report.json"
    if os.path.isfile(jf):
        try:
            with open(jf) as f:
                jd = json.load(f)
            for k, v in jd.items():
                if k.startswith("check_"):
                    nm = k.replace("check_", "").replace("_", ".")
                    assets.append({"name": nm, "type": "http_check",
                        "status": "running" if "PASS" in str(v) else "stopped",
                        "detail": v})
            cd = jd.get("cert_days_left")
            if cd is not None and isinstance(cd, (int, float)):
                assets.append({"name": "SSL_CERT", "type": "certificate",
                    "status": "running" if cd > 7 else "stopped",
                    "detail": str(cd) + " days"})
        except: pass

    # 2) Docker
    try:
        r = __import__("subprocess").run(
            ["docker", "ps", "-a", "--format", "{{.Names}}\t{{.Status}}"],
            capture_output=True, text=True, timeout=10)
        if r.returncode == 0 and r.stdout.strip():
            for ln in r.stdout.strip().split("\n"):
                parts = ln.split("\t")
                if len(parts) >= 2:
                    nm = parts[0].strip().replace("docker-", "")
                    assets.append({"name": nm, "type": "docker",
                        "status": "running" if ("Up" in parts[1] or "Exited (0)" in parts[1]) else "stopped",
                        "detail": parts[1].strip()})
    except: pass

    # 3) System baseline
    bl = {"hostname": "unknown", "platform": "Linux", "arch": "x86_64"}
    try:
        import socket; bl["hostname"] = socket.gethostname()
    except: pass
    try:
        with open("/proc/loadavg") as f:
            bl["loadavg"] = f.read().strip().split()[0]
    except: pass
    try:
        r = __import__("subprocess").run(
            ["df", "-h", "/"], capture_output=True, text=True, timeout=5)
        pts = r.stdout.strip().split("\n")[1].split()
        if len(pts) >= 5:
            bl["disk_total"] = pts[1]; bl["disk_used"] = pts[2]; bl["disk_used_pct"] = pts[4]
    except: pass

    tot = len(assets); run = sum(1 for a in assets if a.get("status") == "running")
    result["nodes"]["master"] = {"host": "localhost", "role": "\u751f\u4ea7\u8282\u70b9",
        "assets": assets, "baseline": bl}
    result["summary"] = {"total_assets": tot, "running": run, "stopped": tot - run,
        "health_pct": round(run / tot * 100, 1) if tot > 0 else 0,
        "overall": "healthy" if run == tot else ("degraded" if run > 0 else "down")}
    return jsonify(result)

    return jsonify({"products": list(state.get("products", {}).values())})

# ── 后台自动巡检 ──

def _auto_scan():
    """Background loop: scan products every 300s (5 min)."""
    LOG_FILE = "/opt/akasha-system/logs/auto_scanner.log"
    os.makedirs("/opt/akasha-system/logs", exist_ok=True)
    while True:
        try:
            results = check_low_health_alert()
            ts = __import__("time").strftime("%Y-%m-%d %H:%M:%S")
            line = "[{}] scanned: {} products, {} alerts\n".format(ts, "all", len(results))
            with open(LOG_FILE, "a") as f:
                f.write(line)
            print("[AUTO-SCAN]", ts, "alerts:", len(results), flush=True)
        except Exception as e:
            print("[AUTO-SCAN] error:", e, flush=True)
        __import__("time").sleep(300)



