"""Tailscale Funnel 自動修復(階段 10-F)。

uvicorn 啟動後在背景跑這個 task,每 5 分鐘 self-check 一次:
  1. 從外部 URL `https://cooper-nas.taila49e28.ts.net/health` curl
  2. 若 200 → 通道好,啥都不做
  3. 若 timeout / TLS handshake fail / 非 200 → 重設 funnel(off + on)
  4. 連續 fix 上限 3 次,免無限亂搞

依賴:
  - robertsu 已被設成 tailscale operator(`sudo tailscale set --operator=robertsu` 已執行)
  - 所以這個 daemon 不用 sudo

每次自動 fix 都會寫 AgentActivity 紀錄,你在 dashboard「庫柏動態」能看到。
"""
from __future__ import annotations

import asyncio
import logging
import subprocess
from datetime import datetime, timezone

logger = logging.getLogger("coba.funnel_watchdog")

TAILSCALE_BIN = "/var/packages/Tailscale/target/bin/tailscale"
FUNNEL_URL = "https://cooper-nas.taila49e28.ts.net/health"
CHECK_INTERVAL_SECONDS = 300       # 5 分鐘
HEALTH_TIMEOUT_SECONDS = 10        # curl 自己的 timeout
PORT = 8000


async def _curl_external() -> tuple[bool, str]:
    """從外部公開 URL curl 自己。回 (ok, msg)。
    ok=True 代表 200 OK;False 代表通道斷或非 200。
    """
    cmd = [
        "curl", "-s", "-o", "/dev/null",
        "-w", "%{http_code}",
        "--max-time", str(HEALTH_TIMEOUT_SECONDS),
        FUNNEL_URL,
    ]
    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=HEALTH_TIMEOUT_SECONDS + 5)
        http_code = stdout.decode().strip()
        if http_code == "200":
            return True, "ok"
        return False, f"http_code={http_code or 'none'}"
    except asyncio.TimeoutError:
        return False, "timeout"
    except Exception as e:
        return False, f"{type(e).__name__}: {e}"


async def _reset_funnel() -> tuple[bool, str]:
    """跑 tailscale funnel off + on。回 (success, msg)。

    operator 已是 robertsu,不需要 sudo。
    """
    try:
        # off
        p1 = await asyncio.create_subprocess_exec(
            TAILSCALE_BIN, "funnel", "off",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        await asyncio.wait_for(p1.communicate(), timeout=15)
        await asyncio.sleep(2)
        # on
        p2 = await asyncio.create_subprocess_exec(
            TAILSCALE_BIN, "funnel", "--bg", str(PORT),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(p2.communicate(), timeout=15)
        out = (stdout + stderr).decode(errors="replace")
        if "Funnel started" in out or "Available on the internet" in out or p2.returncode == 0:
            return True, "funnel restarted"
        return False, f"funnel start unexpected: rc={p2.returncode} out={out[:200]}"
    except Exception as e:
        return False, f"{type(e).__name__}: {e}"


async def _log_activity(summary: str, success: bool, extra: dict | None = None):
    """寫 AgentActivity 讓 dashboard 看得到。"""
    try:
        from app.database import AsyncSessionLocal
        from app.models.agent_activity import AgentActivity
        async with AsyncSessionLocal() as db:
            db.add(AgentActivity(
                activity_type="funnel_watchdog",
                user_id=None,    # 系統動作,無 user
                summary=summary,
                success=success,
                extra=extra or {},
            ))
            await db.commit()
    except Exception:
        logger.exception("funnel_watchdog 寫 activity 失敗")


async def funnel_watchdog_loop():
    """每 5 分鐘檢查一次 funnel。"""
    logger.info("funnel_watchdog_loop 啟動 (check interval=%ds)", CHECK_INTERVAL_SECONDS)
    # 啟動時先等 30 秒,讓 uvicorn 跟 funnel 都穩
    await asyncio.sleep(30)

    consecutive_fails = 0
    while True:
        try:
            ok, msg = await _curl_external()
            if ok:
                if consecutive_fails > 0:
                    logger.info("funnel back ok (after %d fails)", consecutive_fails)
                consecutive_fails = 0
            else:
                consecutive_fails += 1
                logger.warning("[funnel_watchdog] external curl fail: %s (consecutive=%d)",
                               msg, consecutive_fails)
                # 連續 2 次失敗才動手(避免 transient network blip)
                if consecutive_fails >= 2:
                    print(f"[FUNNEL_WATCHDOG] resetting funnel after {consecutive_fails} fails: {msg}", flush=True)
                    fixed, fix_msg = await _reset_funnel()
                    await _log_activity(
                        f"Funnel 通道掉了,自動重啟 ({'成功' if fixed else '失敗'})",
                        success=fixed,
                        extra={
                            "curl_msg": msg,
                            "fix_msg": fix_msg,
                            "consecutive_fails": consecutive_fails,
                        },
                    )
                    if fixed:
                        # 再等 10 秒讓 funnel relay 重連,然後重 check
                        await asyncio.sleep(10)
                        ok2, msg2 = await _curl_external()
                        if ok2:
                            print(f"[FUNNEL_WATCHDOG] ✓ recovered after reset", flush=True)
                            consecutive_fails = 0
                        else:
                            print(f"[FUNNEL_WATCHDOG] ⚠ reset done but still failing: {msg2}", flush=True)
                    # 不管 fix 成功與否,重置計數避免短時間重複嘗試
                    if consecutive_fails > 3:
                        consecutive_fails = 0   # 重置,下個週期再試
        except asyncio.CancelledError:
            logger.info("funnel_watchdog_loop 收到 cancel,退出")
            break
        except Exception:
            logger.exception("funnel_watchdog 迭代例外(繼續下個週期)")

        await asyncio.sleep(CHECK_INTERVAL_SECONDS)
