"""KOGUN Worker v2.0 - 算力贡献守护进程（带自动重连）"""
import asyncio
import json
import logging
import os
import random
import signal
import sys
import time
from datetime import datetime
from pathlib import Path

import httpx

from .device import detect_device
from .executor import execute_task

logger = logging.getLogger("kogun.worker")

CONFIG_DIR = Path.home() / ".kogun"
CONFIG_FILE = CONFIG_DIR / "config.json"
PID_FILE = CONFIG_DIR / "worker.pid"
STATE_FILE = CONFIG_DIR / "state.json"

HEARTBEAT_INTERVAL = 30
TASK_POLL_INTERVAL = 3
MAX_CONSECUTIVE_ERRORS = 10     # 容错更大（配合自动重连）
MAX_RECONNECT_BACKOFF = 60     # 最大重连间隔


# ========== 配置管理 ==========

def load_config() -> dict:
    """加载配置"""
    if CONFIG_FILE.exists():
        return json.loads(CONFIG_FILE.read_text())
    return {}


def save_config(config: dict):
    """保存配置"""
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False))


def load_state() -> dict:
    """加载运行状态"""
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text())
    return {"node_id": None, "started_at": None}


def save_state(state: dict):
    """保存运行状态"""
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ========== HTTP 客户端 ==========

class ApiClient:
    """平台API客户端"""

    def __init__(self, base_url: str, token: str):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }
        self._client = httpx.AsyncClient(
            headers=self.headers,
            timeout=30,
        )

    async def close(self):
        await self._client.aclose()

    async def get(self, path: str) -> dict:
        resp = await self._client.get(f"{self.base_url}{path}")
        data = resp.json()
        if resp.status_code >= 400:
            raise Exception(data.get("detail", f"HTTP {resp.status_code}"))
        return data

    async def post(self, path: str, body: dict = None) -> dict:
        resp = await self._client.post(
            f"{self.base_url}{path}",
            json=body or {},
        )
        data = resp.json()
        if resp.status_code >= 400:
            raise Exception(data.get("detail", f"HTTP {resp.status_code}"))
        return data


# ========== Worker 核心 ==========

class KogunWorker:
    """算力贡献守护进程"""

    def __init__(self, base_url: str, token: str, upstream: dict = None,
                 region: str = "", community: str = ""):
        self.api = ApiClient(base_url, token)
        self.base_url = base_url
        self.token = token
        self.upstream = upstream or {}
        self.region = region
        self.community = community
        self.node_id = None
        self.node_info = None
        self.running = False
        self.consecutive_errors = 0
        self.tasks_completed = 0
        self.total_earned = 0.0
        self.start_time = None

    async def start(self):
        """启动Worker"""
        self.running = True
        self.start_time = time.time()
        self._reconnect_count = 0
        state = load_state()
        self.node_id = state.get("node_id")

        logger.info("=" * 50)
        logger.info("  KOGUN Worker v1.0.0 启动")
        logger.info(f"  服务器: {self.base_url}")
        logger.info("=" * 50)

        try:
            # 注册/验证节点
            if self.node_id:
                logger.info(f"  已有节点ID: {self.node_id}，尝试恢复...")
                try:
                    node_info = await self.api.get(f"/api/devices/{self.node_id}/status")
                    self.node_info = node_info
                    logger.info(f"  节点恢复成功！状态: {node_info.get('status', 'unknown')}")
                except Exception:
                    logger.warning("  节点恢复失败，重新注册")
                    self.node_id = None

            if not self.node_id:
                await self._register_device()

            # 启动协程
            tasks = [
                asyncio.create_task(self._heartbeat_loop()),
                asyncio.create_task(self._task_poll_loop()),
                asyncio.create_task(self._status_report_loop()),
            ]

            # 等待被停止
            try:
                await asyncio.gather(*tasks)
            except asyncio.CancelledError:
                logger.info("Worker 协程被取消")
                for t in tasks:
                    t.cancel()
                await asyncio.gather(*tasks, return_exceptions=True)

        except Exception as e:
            logger.error(f"Worker 异常: {e}")
            raise
        finally:
            await self.api.close()
            self.running = False

    def stop(self):
        """停止Worker"""
        self.running = False
        logger.info("正在停止 Worker...")

    async def _register_device(self):
        """向平台注册本机设备"""
        logger.info("  检测本机硬件...")
        device_info = detect_device()
        logger.info(f"  CPU: {device_info.get('cpu_model', 'Unknown')} ({device_info.get('cpu_cores', 0)}核)")
        logger.info(f"  GPU: {device_info.get('gpu_model', 'Unknown')}")
        logger.info(f"  内存: {device_info.get('ram_mb', 0) // 1024}GB")

        # 注册节点
        logger.info("  注册节点到平台...")
        result = await self.api.post("/api/devices/register", {
            "gpu_name": device_info.get("gpu_model", "Unknown")[:100],
            "cpu_cores": device_info.get("cpu_cores", 0),
            "ram_gb": round(device_info.get("ram_mb", 0) / 1024, 1),
            "os_info": device_info.get("os_info", "")[:200],
            "device_type": device_info.get("device_type", "pc"),
            "network_type": device_info.get("network_type", "unknown"),
            "region": self.region or "",
            "community": self.community or "",
        })

        self.node_id = result.get("node_id")
        verify_code = result.get("verify_code", "")
        fingerprint = result.get("fingerprint", "")

        # 验证节点
        logger.info(f"  验证节点 #{self.node_id}...")
        await self.api.post(f"/api/devices/{self.node_id}/verify", {
            "code": verify_code,
        })

        # 保存状态
        save_state({"node_id": self.node_id, "started_at": datetime.now().isoformat()})
        self.node_info = {"id": self.node_id, "gpu_name": device_info.get("gpu_model", "Unknown")}
        self.consecutive_errors = 0

        logger.info(f"  ✅ 节点 #{self.node_id} 注册成功！指纹: {fingerprint[:16]}...")

    async def _heartbeat_loop(self):
        """心跳循环 — 带指数退避重连"""
        while self.running:
            try:
                await asyncio.sleep(HEARTBEAT_INTERVAL)
                if not self.node_id:
                    continue

                import psutil
                gpu_load = 0
                cpu_load = psutil.cpu_percent(interval=0.5)

                # 尝试获取GPU负载
                try:
                    r = await asyncio.create_subprocess_exec(
                        "nvidia-smi", "--query-gpu=utilization.gpu",
                        "--format=csv,noheader,nounits",
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.PIPE,
                    )
                    out, _ = await asyncio.wait_for(r.communicate(), timeout=5)
                    if out:
                        gpu_load = float(out.decode().strip().split("\n")[0])
                except Exception:
                    pass

                await self.api.post(f"/api/devices/{self.node_id}/heartbeat", {
                    "gpu_load": gpu_load,
                    "cpu_load": cpu_load,
                })

                self.consecutive_errors = 0
                self._reconnect_count = 0
                uptime = int(time.time() - self.start_time)
                logger.debug(f"💓 心跳 #{self.node_id} OK (上线 {uptime // 60}分钟)")

            except asyncio.CancelledError:
                break
            except (httpx.ConnectError, httpx.TimeoutException) as e:
                self.consecutive_errors += 1
                self._reconnect_count += 1
                backoff = min(2 ** (self._reconnect_count - 1), MAX_RECONNECT_BACKOFF)
                jitter = random.uniform(0.8, 1.2)
                sleep_time = backoff * jitter
                logger.warning(f"📡 心跳连接断开 (第{self._reconnect_count}次重连, {sleep_time:.0f}s后)")
                await asyncio.sleep(sleep_time)
                if self.consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
                    logger.error("❌ 心跳连续失败，Worker停止")
                    self.running = False
                    break
            except Exception as e:
                self.consecutive_errors += 1
                logger.warning(f"⚠️  心跳异常 ({self.consecutive_errors}/{MAX_CONSECUTIVE_ERRORS}): {e}")
                if self.consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
                    logger.error("❌ 心跳连续失败，Worker停止")
                    self.running = False
                    break

    async def _task_poll_loop(self):
        """任务轮询循环 — 带自动重连"""
        await asyncio.sleep(5)  # 等心跳先起来

        while self.running:
            try:
                if not self.node_id:
                    await asyncio.sleep(5)
                    continue

                # 查询分配给本节点的任务（由调度器推送的）
                pool = await self.api.get(f"/api/tasks/pool/assigned/{self.node_id}")
                self._reconnect_count = 0
                tasks = pool.get("tasks", [])

                if not tasks:
                    continue

                logger.info(f"📦 收到 {len(tasks)} 个推送任务！")

                for task in tasks:
                    if not self.running:
                        break

                    task_id = task["id"]
                    try:
                        await self._handle_pushed_task(task_id)
                    except Exception as e:
                        logger.error(f"  任务 #{task_id} 处理失败: {e}")
                        try:
                            await self.api.post(f"/api/tasks/{task_id}/reject", {
                                "node_id": self.node_id,
                                "reason": str(e)[:200],
                            })
                        except Exception:
                            pass

                # 后备：也检查通用任务池（兼容旧版）
                try:
                    pool2 = await self.api.get("/api/tasks/pool")
                    fallback_tasks = pool2.get("tasks", [])
                    for task in fallback_tasks:
                        if not self.running:
                            break
                        task_id = task["id"]
                        if not any(t.get("id") == task_id for t in tasks):  # 避免重复
                            logger.info(f"📦 通用池发现任务 #{task_id}，尝试接收")
                            await self._handle_pushed_task(task_id)
                except Exception:
                    pass

                await asyncio.sleep(TASK_POLL_INTERVAL)

            except asyncio.CancelledError:
                break
            except Exception as e:
                self._reconnect_count += 1
                backoff = min(2 ** (self._reconnect_count - 1), MAX_RECONNECT_BACKOFF)
                jitter = random.uniform(0.8, 1.2)
                sleep_time = backoff * jitter
                logger.warning(f"📡 任务轮询断开 (第{self._reconnect_count}次重连, {sleep_time:.0f}s后): {e}")
                await asyncio.sleep(sleep_time)

    async def _handle_pushed_task(self, task_id: int):
        """处理已推送到本节点的任务"""
        logger.info(f"🔧 处理推送任务 #{task_id}...")

        # 获取任务详情（包含prompt等敏感信息）
        task = await self.api.get(f"/api/tasks/{task_id}")
        logger.info(f"  类型: {task.get('type', 'unknown')} | 模型: {task.get('model', '')}")

        # 接受任务（assigned → running）
        accept_result = await self.api.post(f"/api/tasks/{task_id}/accept", {
            "node_id": self.node_id,
        })
        logger.info(f"  ✅ 已接受任务 #{task_id}")

        # 执行任务
        logger.info(f"  ⏳ 执行中...")
        exec_result = await execute_task(task, self.upstream)

        # 完成任务
        complete_result = await self.api.post(f"/api/tasks/{task_id}/complete", {
            "actual_cost": exec_result["actual_cost"],
            "result": exec_result["result"],
            "duration_seconds": exec_result.get("duration_seconds", 60),
        })

        earnings = complete_result.get("earnings", 0)
        self.tasks_completed += 1
        self.total_earned += earnings

        logger.info(f"  ✅ 任务 #{task_id} 完成！")
        logger.info(f"     ⏱ 耗时: {exec_result.get('duration_seconds', 0):.1f}s")
        logger.info(f"     💰 收益: ¥{earnings:.4f}")
        logger.info(f"     📊 累计: ¥{self.total_earned:.4f} ({self.tasks_completed}个任务)")

    async def _status_report_loop(self):
        """状态汇报循环（每60秒）"""
        while self.running:
            try:
                await asyncio.sleep(60)
                if self.tasks_completed > 0:
                    uptime = int(time.time() - self.start_time)
                    hourly_rate = self.total_earned / (uptime / 3600) if uptime > 0 else 0
                    logger.info(
                        f"📊 运行报告 | 上线 {uptime // 60}分钟 | "
                        f"任务: {self.tasks_completed}个 | "
                        f"收益: ¥{self.total_earned:.4f} | "
                        f"时均: ¥{hourly_rate:.4f}/h"
                    )
            except asyncio.CancelledError:
                break
            except Exception:
                pass


# ========== 守护进程管理 ==========

def is_worker_running() -> bool:
    """检查Worker是否在运行"""
    if PID_FILE.exists():
        try:
            pid = int(PID_FILE.read_text().strip())
            os.kill(pid, 0)  # 检查进程是否存在
            return True
        except (ProcessLookupError, ValueError, OSError):
            PID_FILE.unlink(missing_ok=True)
    return False


def write_pid():
    """写入PID文件"""
    PID_FILE.write_text(str(os.getpid()))


def remove_pid():
    """删除PID文件"""
    PID_FILE.unlink(missing_ok=True)


def get_worker_status() -> dict:
    """获取Worker运行状态"""
    state = load_state()
    config = load_config()

    if not is_worker_running():
        return {"running": False, "node_id": state.get("node_id")}

    return {
        "running": True,
        "node_id": state.get("node_id"),
        "pid": int(PID_FILE.read_text()) if PID_FILE.exists() else None,
        "server": config.get("server", ""),
    }
