// probe_workersai.go — 探測「這個帳號的雲端知識庫，Workers AI 萃取通了沒」（t182）。
//
// 🔴 為什麼要探測（leo 2026-08-04 兩句話定的設計）：
//
//	① 「順序不可反這件事**門檻太高**了，用戶一定**先看到 daemon 有更新**，
//	    不會想到更新網頁」
//	② 「你可以設計如果它先做了地端就要求去做雲端，**會去掃一次看雲端是否裝好**，
//	    **沒裝好就顯示 workers AI 還沒通，一旦通了就顯示可用**」
//
// 背景：Workers AI 萃取要**兩端都新**——daemon 有新版（會打 /portal/daemon/extract）
// 且雲端實例也有新版（有這條 route）。daemon 會自動更新、雲端實例要用戶自己重跑安裝器
// ⇒ 中間必然存在一段「daemon 新、雲端舊」的空窗期。
//
// 舊設計把這段空窗丟給用戶自己想通（「你順序做錯了」），那是**我們的設計沒容錯**。
// 新設計：daemon 主動掃、把狀態講出來——
//
//	雲端還沒更新 → 托盤直接顯示「雲端 AI 還沒通 ⇒ 去 portal 按『立即更新』」
//	雲端已更新   → 顯示「雲端 AI 可用」，什麼都不必做
//
// ⚠️ 這是**被動探測**，不是輪詢：只在同步時（RunDirectOnce 每輪一次）順手打一次
// HEAD-ish 的探測請求，且結果快取在 status.json 給托盤讀。不掛任何排程。
package collector

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"
)

// probeHTTP 探測用短逾時：只是要知道 route 在不在，不等 LLM 生成。
//
// 🔴 `inkstone/arcrun-rag#153`：這把 20 秒是保險，真正的上限由呼叫端帶進來的
// context 決定。探測是**每輪、每個帳號的第一發**——它撞到不回應的端點時，
// 後面所有帳號都還沒開始，所以它也是最早能認出「這個帳號今天不回應」的位置。
var probeHTTP = &http.Client{Timeout: 20 * time.Second}

// CloudAIState 是「某個雲端實例的 Workers AI 萃取能力」探測結果。
type CloudAIState struct {
	Ready bool   `json:"ready"`           // true＝那端有 /portal/daemon/extract（雲端 AI 可用）
	Note  string `json:"note,omitempty"`  // 給用戶看的白話（Ready=false 時說「還沒通、該做什麼」）
}

// ProbeWorkersAI 探測指定實例是否支援雲端萃取。
//
// 手法：送一個**刻意留空 text** 的請求。
//   - 舊版實例沒有這條 route ⇒ 404 ⇒ 還沒通。
//   - 新版實例有 route，會因為 text 是空的回 400（參數錯）——但 400 正好證明**route 存在**
//     ⇒ 算通。這樣探測不會真的燒 LLM 額度，也不會產生垃圾卡片。
//   - 401 代表 route 在、只是金鑰不對 ⇒ route 存在，同樣算「雲端有這功能」，
//     金鑰問題由既有的連線流程去報，不混在這裡講。
func ProbeWorkersAI(ctx context.Context, cypherURL, apiKey string) (CloudAIState, error) {
	base := strings.TrimSuffix(strings.TrimSpace(cypherURL), "/")
	if base == "" {
		return CloudAIState{Ready: false, Note: "還沒連上知識庫"}, nil
	}

	body, _ := json.Marshal(map[string]string{"page_name": "", "text": ""})
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/portal/daemon/extract", bytes.NewReader(body))
	if err != nil {
		return CloudAIState{Ready: false, Note: "雲端 AI 狀態查不到"}, nil
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Arcrun-API-Key", strings.TrimSpace(apiKey))

	resp, err := probeHTTP.Do(req)
	if err != nil {
		// #153：錯誤原件一起回去，呼叫端才分得出「等到超時」與「立刻連不上」——
		// 前者要記進斷路器，後者不該（不然一次斷網就把帳號判成停機）。
		// 連不上（離線／網路問題）≠ 雲端沒裝。講「查不到」而不是「還沒通」，
		// 免得把網路問題誤報成「你沒更新」讓用戶白跑一趟。
		return CloudAIState{Ready: false, Note: "連不上你的知識庫，雲端 AI 狀態查不到"}, err
	}
	defer resp.Body.Close()

	switch {
	case resp.StatusCode == http.StatusNotFound:
		return CloudAIState{
			Ready: false,
			Note:  "雲端 AI 還沒通 ⇒ 你的知識庫是舊版，請到 portal 按「立即更新」重裝一次",
		}, nil
	case resp.StatusCode < 500:
		// 200/400/401… 都代表這條 route 存在＝雲端有這個功能。
		return CloudAIState{Ready: true}, nil
	default:
		return CloudAIState{
			Ready: false,
			Note:  fmt.Sprintf("雲端 AI 暫時有狀況（HTTP %d），稍後會自動再試", resp.StatusCode),
		}, nil
	}
}

// probeWorkersAI＝帶「等待閘」的探測（`inkstone/arcrun-rag#153`）。
//
// 為什麼這一發特別重要：它是每輪、每個帳號的**第一發**。撞到不回應的端點時，
// 後面的資料夾一件都還沒開始 ⇒ 這裡認出來，整個帳號的其餘工作就都省下來了。
// 反過來說，漏掉這一發的話，前面所有的閘都白裝——2026-08-28 第一版就是這樣：
// 斷路器跳了，可是一輪還是要 20 秒，因為那 20 秒全花在這一發上。
func (c *DirectConfig) probeWorkersAI() CloudAIState {
	if note := c.unreachableNote(); note != "" {
		return CloudAIState{Ready: false, Note: note}
	}
	gate := c.openGate(stepProbeAI)
	state, err := ProbeWorkersAI(gate.ctx, c.CypherURL, c.APIKey)
	gate.release()
	if perr := gate.record(err); perr != nil {
		// 等到超時 ⇒ 講的是「沒有回應」，不是「你的雲端沒裝好」。
		// 把等待誤報成「你沒更新」會害使用者白跑一趟去按重裝。
		return CloudAIState{Ready: false, Note: perr.Error()}
	}
	return state
}
