package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	goruntime "runtime"
	"strings"
	"sync"
	"time"

	"lingma-ipc-proxy/internal/httpapi"
	"lingma-ipc-proxy/internal/lingmaipc"
	"lingma-ipc-proxy/internal/remote"
	"lingma-ipc-proxy/internal/service"

	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
// RequestRecord stores a single HTTP request summary
type RequestRecord struct {
	Time         string `json:"time"`
	Method       string `json:"method"`
	Path         string `json:"path"`
	Model        string `json:"model,omitempty"`
	StatusCode   int    `json:"statusCode"`
	Duration     string `json:"duration"`
	Size         string `json:"size,omitempty"`
	InputTokens  int    `json:"inputTokens,omitempty"`
	OutputTokens int    `json:"outputTokens,omitempty"`
	TotalTokens  int    `json:"totalTokens,omitempty"`
	ReqBody      string `json:"reqBody,omitempty"`
	RespBody     string `json:"respBody,omitempty"`
}

type AppLog struct {
	Time    string `json:"time"`
	Level   string `json:"level"`
	Message string `json:"message"`
}

type TokenStats struct {
	TotalRequests   int            `json:"totalRequests"`
	SuccessRequests int            `json:"successRequests"`
	InputTokens     int            `json:"inputTokens"`
	OutputTokens    int            `json:"outputTokens"`
	TotalTokens     int            `json:"totalTokens"`
	ByModel         map[string]int `json:"byModel,omitempty"`
	LastModel       string         `json:"lastModel,omitempty"`
	LastUpdated     string         `json:"lastUpdated,omitempty"`
}

type App struct {
	ctx context.Context

	mu        sync.RWMutex
	cfg       service.Config
	server    *httpapi.Server
	running   bool
	quitting  bool
	addr      string
	startedAt time.Time
	quitHint  time.Time
	models    []ModelInfo
	requests  []RequestRecord
	logs      []AppLog
	stats     TokenStats
}

// ModelInfo represents a model returned by /v1/models
type ModelInfo struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// ProxyStatus represents the current proxy status
type ProxyStatus struct {
	Running   bool   `json:"running"`
	Addr      string `json:"addr"`
	Backend   string `json:"backend"`
	Models    int    `json:"models"`
	Model     string `json:"model,omitempty"`
	StartedAt string `json:"startedAt,omitempty"`
}

// DetectionInfo exposes non-sensitive resolved connection details for the UI.
type DetectionInfo struct {
	ListenURL               string `json:"listenUrl"`
	Backend                 string `json:"backend"`
	BackendLabel            string `json:"backendLabel"`
	IPCSuccess              bool   `json:"ipcSuccess"`
	IPCTransport            string `json:"ipcTransport,omitempty"`
	IPCEndpoint             string `json:"ipcEndpoint,omitempty"`
	IPCError                string `json:"ipcError,omitempty"`
	RemoteBaseURL           string `json:"remoteBaseUrl"`
	RemoteBaseURLSource     string `json:"remoteBaseUrlSource,omitempty"`
	RemoteCredentialSuccess bool   `json:"remoteCredentialSuccess"`
	RemoteCredentialSource  string `json:"remoteCredentialSource,omitempty"`
	RemoteUserID            string `json:"remoteUserId,omitempty"`
	RemoteMachineID         string `json:"remoteMachineId,omitempty"`
	RemoteTokenExpireAt     string `json:"remoteTokenExpireAt,omitempty"`
	RemoteTokenExpired      bool   `json:"remoteTokenExpired"`
	RemoteCredentialError   string `json:"remoteCredentialError,omitempty"`
}

// NewApp creates a new App application struct
func NewApp() *App {
	return &App{}
}

// startup is called when the app starts
func (a *App) startup(ctx context.Context) {
	a.ctx = ctx
	a.cfg = defaultConfig()
	if err := a.loadAppState(); err != nil {
		runtime.LogWarningf(a.ctx, "failed to load app state: %v", err)
	}

	// Auto-save default config on first run so users can find/edit it later
	if err := a.saveConfig(a.cfg); err != nil {
		runtime.LogWarningf(a.ctx, "failed to save default config: %v", err)
	}

	// Auto-start proxy so the app is usable immediately
	go func() {
		if err := a.StartProxy(); err != nil {
			a.emitLog("error", fmt.Sprintf("Auto-start failed: %v. %s", err, transportFallbackHint()))
		} else {
			a.emitLog("info", "Proxy auto-started")
		}
	}()
}

// onDomReady is called when the frontend DOM is ready
func (a *App) onDomReady(ctx context.Context) {
	a.ctx = ctx
}

// onSecondInstanceLaunch is called when user clicks the dock icon while app is already running.
// We show the window so the user can interact with it again.
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
	a.ShowWindow()
}

// beforeClose hides the window by default so the proxy can keep running.
// QuitApp sets quitting=true before allowing the process to exit.
func (a *App) beforeClose(ctx context.Context) bool {
	a.mu.Lock()
	if a.quitting {
		a.mu.Unlock()
		return true
	}

	now := time.Now()
	if !a.quitHint.IsZero() && now.Sub(a.quitHint) <= 2*time.Second {
		a.mu.Unlock()
		go a.forceQuit()
		return true
	}
	a.quitHint = now
	a.mu.Unlock()

	message := "再按一次退出快捷键将停止代理并退出应用"
	a.emitLog("warn", message)
	runtime.EventsEmit(a.ctx, "quit:confirm", message)
	return true
}

// ShowWindow shows the main window
func (a *App) ShowWindow() {
	runtime.Show(a.ctx)
	runtime.WindowShow(a.ctx)
	runtime.WindowUnminimise(a.ctx)
}

// HideWindow hides the main window
func (a *App) HideWindow() {
	runtime.Hide(a.ctx)
}

// MinimizeWindow minimises the main window.
func (a *App) MinimizeWindow() {
	runtime.WindowMinimise(a.ctx)
}

func (a *App) beginQuit() {
	go a.forceQuit()
}

// QuitApp fully quits the application
func (a *App) QuitApp() {
	a.beginQuit()
}

// ForceQuitApp stops the proxy and exits the desktop process immediately.
func (a *App) ForceQuitApp() {
	a.beginQuit()
}

// RequestQuitShortcut requires two shortcut presses to avoid accidental exits.
func (a *App) RequestQuitShortcut() {
	now := time.Now()
	a.mu.Lock()
	shouldQuit := !a.quitHint.IsZero() && now.Sub(a.quitHint) <= 2*time.Second
	a.quitHint = now
	a.mu.Unlock()

	if shouldQuit {
		go a.forceQuit()
		return
	}

	message := "再按一次退出快捷键将停止代理并退出应用"
	a.emitLog("warn", message)
	runtime.EventsEmit(a.ctx, "quit:confirm", message)
}

func (a *App) forceQuit() {
	a.mu.Lock()
	if a.quitting {
		a.mu.Unlock()
		return
	}
	a.quitting = true
	a.mu.Unlock()

	a.emitLog("info", "正在停止代理并退出应用")

	done := make(chan struct{})
	go func() {
		if err := a.StopProxy(); err != nil {
			runtime.LogWarningf(a.ctx, "stop proxy before exit failed: %v", err)
		}
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(1200 * time.Millisecond):
		runtime.LogWarning(a.ctx, "force quit continuing before proxy shutdown completed")
	}
	os.Exit(0)
}

func (a *App) emitLog(level string, message string) {
	entry := AppLog{
		Time:    time.Now().Format("15:04:05"),
		Level:   level,
		Message: message,
	}
	a.mu.Lock()
	a.logs = append(a.logs, entry)
	if len(a.logs) > 2000 {
		a.logs = a.logs[len(a.logs)-2000:]
	}
	a.saveAppStateLocked()
	a.mu.Unlock()
	runtime.EventsEmit(a.ctx, "log", entry)
}

// GetStatus returns the current proxy status
func (a *App) GetStatus() ProxyStatus {
	a.mu.RLock()
	defer a.mu.RUnlock()
	startedAt := ""
	if !a.startedAt.IsZero() {
		startedAt = a.startedAt.Format(time.RFC3339)
	}
	return ProxyStatus{
		Running:   a.running,
		Addr:      a.addr,
		Backend:   string(a.cfg.Backend),
		Models:    len(a.models),
		Model:     a.cfg.Model,
		StartedAt: startedAt,
	}
}

// GetConfig returns the current configuration.
// Timeout is returned in seconds for frontend convenience.
func (a *App) GetConfig() service.Config {
	a.mu.RLock()
	cfg := a.cfg
	a.mu.RUnlock()
	cfg.Timeout = cfg.Timeout / time.Second
	return cfg
}

// GetDetectionInfo returns resolved IPC/remote details without exposing tokens.
func (a *App) GetDetectionInfo() DetectionInfo {
	a.mu.RLock()
	cfg := a.cfg
	addr := a.addr
	a.mu.RUnlock()

	if strings.TrimSpace(addr) == "" {
		addr = fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
	}
	baseURL := remote.ResolveBaseURLWithSource(cfg.RemoteBaseURL)
	info := DetectionInfo{
		ListenURL:           "http://" + addr,
		Backend:             string(cfg.Backend),
		BackendLabel:        backendLabel(cfg.Backend),
		RemoteBaseURL:       baseURL.URL,
		RemoteBaseURLSource: baseURL.Source,
	}

	if opts, err := lingmaipc.ResolveDialOptions(cfg.Transport, cfg.Pipe, cfg.WebSocketURL); err == nil {
		info.IPCSuccess = true
		info.IPCTransport = string(opts.Transport)
		switch opts.Transport {
		case lingmaipc.TransportPipe:
			info.IPCEndpoint = opts.PipePath
		case lingmaipc.TransportWebSocket:
			info.IPCEndpoint = opts.WebSocketURL
		}
	} else {
		info.IPCError = err.Error()
	}

	if cred, err := remote.LoadCredential(cfg.RemoteAuthFile); err == nil {
		info.RemoteCredentialSuccess = true
		info.RemoteCredentialSource = cred.Source
		info.RemoteUserID = maskIdentifier(cred.UserID)
		info.RemoteMachineID = maskIdentifier(cred.MachineID)
		info.RemoteTokenExpired = remote.IsExpired(cred, 0)
		if cred.TokenExpireTime > 0 {
			info.RemoteTokenExpireAt = time.UnixMilli(cred.TokenExpireTime).Format(time.RFC3339)
		}
	} else {
		info.RemoteCredentialError = err.Error()
	}

	return info
}

// UpdateConfig updates the configuration, saves to file, and restarts the proxy if running.
// Frontend sends Timeout in seconds; we convert to time.Duration.
func (a *App) UpdateConfig(cfg service.Config) error {
	// Convert seconds -> Duration if frontend sent a small value
	if cfg.Timeout > 0 && cfg.Timeout < time.Second {
		cfg.Timeout = cfg.Timeout * time.Second
	}

	a.mu.Lock()
	wasRunning := a.running
	a.cfg = cfg
	a.mu.Unlock()

	if err := a.saveConfig(cfg); err != nil {
		runtime.LogWarningf(a.ctx, "failed to save config: %v", err)
		a.emitLog("warn", fmt.Sprintf("Config updated but failed to save: %v", err))
	} else {
		a.emitLog("info", "Config saved to file")
	}

	if wasRunning {
		if err := a.StopProxy(); err != nil {
			return fmt.Errorf("stop failed: %w", err)
		}
		return a.StartProxy()
	}
	return nil
}

func (a *App) saveConfig(cfg service.Config) error {
	home, err := os.UserHomeDir()
	if err != nil {
		return err
	}
	dir := filepath.Join(home, ".config", "lingma-proxy")
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	timeoutSec := int(cfg.Timeout.Seconds())
	fileCfg := map[string]any{
		"host":                    cfg.Host,
		"port":                    cfg.Port,
		"backend":                 string(cfg.Backend),
		"transport":               string(cfg.Transport),
		"pipe":                    cfg.Pipe,
		"websocket_url":           cfg.WebSocketURL,
		"remote_base_url":         cfg.RemoteBaseURL,
		"remote_auth_file":        cfg.RemoteAuthFile,
		"remote_version":          cfg.RemoteVersion,
		"cwd":                     cfg.Cwd,
		"current_file_path":       cfg.CurrentFilePath,
		"mode":                    cfg.Mode,
		"model":                   cfg.Model,
		"shell_type":              cfg.ShellType,
		"session_mode":            string(cfg.SessionMode),
		"timeout":                 timeoutSec,
		"remote_fallback_enabled": cfg.RemoteFallbackEnabled,
		"remote_fallback_models":  cfg.RemoteFallbackModels,
	}

	data, err := json.MarshalIndent(fileCfg, "", "  ")
	if err != nil {
		return err
	}

	path := filepath.Join(dir, "config.json")
	return os.WriteFile(path, data, 0644)
}

// StartProxy starts the lingma-ipc-proxy HTTP server
func (a *App) StartProxy() error {
	a.mu.Lock()
	if a.running {
		a.mu.Unlock()
		return fmt.Errorf("proxy already running")
	}

	addr := fmt.Sprintf("%s:%d", a.cfg.Host, a.cfg.Port)
	cfg := a.cfg
	a.mu.Unlock()

	svc := service.New(cfg)

	warmupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	if err := svc.Warmup(warmupCtx); err != nil {
		runtime.LogWarningf(a.ctx, "warmup failed: %v", err)
		a.emitLog("warn", fmt.Sprintf("%s warmup failed: %v. %s", backendLabel(cfg.Backend), err, warmupFallbackHint(cfg.Backend)))
	} else {
		runtime.LogInfof(a.ctx, "%s warmup completed", backendLabel(cfg.Backend))
		a.emitLog("info", fmt.Sprintf("%s warmup completed", backendLabel(cfg.Backend)))
	}
	cancel()

	server := httpapi.NewServer(addr, svc)
	server.OnRequest = func(method, path string, statusCode int, duration time.Duration, reqBody, respBody string) {
		inputTokens, outputTokens := extractTokenUsage(respBody)
		model := extractRequestModel(reqBody)
		record := RequestRecord{
			Time:         time.Now().Format("15:04:05"),
			Method:       method,
			Path:         path,
			Model:        model,
			StatusCode:   statusCode,
			Duration:     duration.Round(time.Millisecond).String(),
			Size:         formatPayloadSize(len(reqBody) + len(respBody)),
			InputTokens:  inputTokens,
			OutputTokens: outputTokens,
			TotalTokens:  inputTokens + outputTokens,
			ReqBody:      reqBody,
			RespBody:     respBody,
		}
		a.mu.Lock()
		a.requests = append(a.requests, record)
		if len(a.requests) > 2000 {
			a.requests = a.requests[len(a.requests)-2000:]
		}
		a.accumulateTokenStatsLocked(record)
		a.saveAppStateLocked()
		a.mu.Unlock()
		runtime.EventsEmit(a.ctx, "requests:updated", a.GetRequests())
		runtime.EventsEmit(a.ctx, "usage:updated", a.GetTokenStats())
	}

	// Check if the port is available before claiming we're running
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return fmt.Errorf("port %s is already in use: %w", addr, err)
	}
	ln.Close()

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			runtime.LogErrorf(a.ctx, "server error: %v", err)
			a.emitLog("error", fmt.Sprintf("Server error: %v", err))
			a.mu.Lock()
			a.running = false
			a.addr = ""
			a.startedAt = time.Time{}
			a.mu.Unlock()
		}
	}()

	a.mu.Lock()
	if a.running {
		a.mu.Unlock()
		return fmt.Errorf("proxy already running")
	}
	a.server = server
	a.addr = addr
	a.running = true
	a.startedAt = time.Now()
	a.mu.Unlock()

	msg := fmt.Sprintf("Proxy started on http://%s", addr)
	runtime.LogInfof(a.ctx, msg)
	a.emitLog("info", msg)

	// Fetch models in background
	go a.fetchModels(addr)

	return nil
}

func (a *App) GetLogs() []AppLog {
	a.mu.RLock()
	defer a.mu.RUnlock()
	out := make([]AppLog, len(a.logs))
	copy(out, a.logs)
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out
}

func (a *App) ClearLogs() {
	a.mu.Lock()
	a.logs = nil
	a.saveAppStateLocked()
	a.mu.Unlock()
	runtime.EventsEmit(a.ctx, "logs:updated", a.GetLogs())
}

// StopProxy stops the proxy server
func (a *App) StopProxy() error {
	a.mu.Lock()
	if !a.running || a.server == nil {
		a.mu.Unlock()
		return nil
	}

	server := a.server
	a.server = nil
	a.running = false
	a.addr = ""
	a.startedAt = time.Time{}
	a.models = nil
	a.mu.Unlock()

	runtime.EventsEmit(a.ctx, "status:updated", a.GetStatus())

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		a.emitLog("warn", fmt.Sprintf("Proxy stop forced after graceful shutdown timeout: %v", err))
		return err
	}

	runtime.LogInfo(a.ctx, "proxy stopped")
	a.emitLog("info", "Proxy stopped")
	return nil
}

// GetModels returns the cached model list
func (a *App) GetModels() []ModelInfo {
	a.mu.RLock()
	defer a.mu.RUnlock()
	return a.models
}

// GetRequests returns recent HTTP request records
func (a *App) GetRequests() []RequestRecord {
	a.mu.RLock()
	defer a.mu.RUnlock()
	out := make([]RequestRecord, len(a.requests))
	copy(out, a.requests)
	// reverse so newest first
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out
}

// ClearRequests clears the request history
func (a *App) ClearRequests() {
	a.mu.Lock()
	a.requests = nil
	a.saveAppStateLocked()
	a.mu.Unlock()
	a.emitLog("info", "Request history cleared")
}

func (a *App) GetTokenStats() TokenStats {
	a.mu.RLock()
	defer a.mu.RUnlock()
	stats := a.stats
	if stats.ByModel != nil {
		stats.ByModel = cloneIntMap(stats.ByModel)
	}
	return stats
}

// RefreshModels probes the running proxy for the latest model list.
func (a *App) RefreshModels() ([]ModelInfo, error) {
	a.mu.RLock()
	running := a.running
	addr := a.addr
	a.mu.RUnlock()

	if !running || addr == "" {
		return nil, fmt.Errorf("proxy is not running")
	}

	models, err := a.fetchModels(addr)
	if err != nil {
		return nil, err
	}
	return models, nil
}

func (a *App) SelectModel(modelID string) (ProxyStatus, error) {
	modelID = strings.TrimSpace(modelID)
	if modelID == "" {
		return a.GetStatus(), fmt.Errorf("model id is required")
	}

	a.mu.Lock()
	found := len(a.models) == 0
	for _, model := range a.models {
		if model.ID == modelID {
			found = true
			break
		}
	}
	if !found {
		a.mu.Unlock()
		return a.GetStatus(), fmt.Errorf("model %q is not in the detected model list", modelID)
	}
	a.cfg.Model = modelID
	cfg := a.cfg
	server := a.server
	a.mu.Unlock()

	if server != nil {
		server.SetDefaultModel(modelID)
	}
	if err := a.saveConfig(cfg); err != nil {
		a.emitLog("warn", fmt.Sprintf("Model switched but config save failed: %v", err))
	}
	a.emitLog("info", fmt.Sprintf("已切换默认模型：%s", modelID))
	return a.GetStatus(), nil
}

func (a *App) fetchModels(addr string) ([]ModelInfo, error) {
	url := fmt.Sprintf("http://%s/v1/models", addr)
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		runtime.LogWarningf(a.ctx, "fetch models failed: %v", err)
		return nil, err
	}
	defer resp.Body.Close()

	var result struct {
		Data []struct {
			ID   string `json:"id"`
			Name string `json:"name"`
		} `json:"data"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		runtime.LogWarningf(a.ctx, "decode models failed: %v", err)
		return nil, err
	}

	models := make([]ModelInfo, 0, len(result.Data))
	for _, m := range result.Data {
		models = append(models, ModelInfo{ID: m.ID, Name: m.Name})
	}

	a.mu.Lock()
	a.models = models
	a.mu.Unlock()

	runtime.EventsEmit(a.ctx, "models:updated", models)
	if len(models) > 0 {
		a.emitLog("info", fmt.Sprintf("Loaded %d models", len(models)))
	}
	return models, nil
}

func extractRequestModel(reqBody string) string {
	if strings.TrimSpace(reqBody) == "" {
		return ""
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(reqBody), &payload); err != nil {
		return ""
	}
	if model, ok := payload["model"].(string); ok {
		return strings.TrimSpace(model)
	}
	return ""
}

func formatPayloadSize(bytes int) string {
	if bytes <= 0 {
		return "-"
	}
	const kb = 1024
	const mb = 1024 * kb
	if bytes >= mb {
		return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
	}
	if bytes >= kb {
		return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
	}
	return fmt.Sprintf("%d B", bytes)
}

type appStateFile struct {
	Requests []RequestRecord `json:"requests"`
	Logs     []AppLog        `json:"logs"`
	Stats    TokenStats      `json:"stats"`
}

func (a *App) loadAppState() error {
	path, err := appStatePath()
	if err != nil {
		return err
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	var state appStateFile
	if err := json.Unmarshal(data, &state); err != nil {
		return err
	}
	a.mu.Lock()
	defer a.mu.Unlock()
	a.requests = state.Requests
	a.logs = state.Logs
	a.stats = state.Stats
	if a.stats.ByModel == nil {
		a.stats.ByModel = map[string]int{}
	}
	a.reconcileTokenStatsLocked()
	return nil
}

func (a *App) saveAppStateLocked() {
	path, err := appStatePath()
	if err != nil {
		runtime.LogWarningf(a.ctx, "resolve app state path failed: %v", err)
		return
	}
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		runtime.LogWarningf(a.ctx, "create app state dir failed: %v", err)
		return
	}
	state := appStateFile{
		Requests: a.requests,
		Logs:     a.logs,
		Stats:    a.stats,
	}
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		runtime.LogWarningf(a.ctx, "marshal app state failed: %v", err)
		return
	}
	if err := os.WriteFile(path, data, 0644); err != nil {
		runtime.LogWarningf(a.ctx, "write app state failed: %v", err)
	}
}

func appStatePath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".config", "lingma-ipc-proxy", "app-state.json"), nil
}

func (a *App) accumulateTokenStatsLocked(record RequestRecord) {
	a.stats.TotalRequests++
	if record.StatusCode >= 200 && record.StatusCode < 300 {
		a.stats.SuccessRequests++
	}
	a.stats.InputTokens += record.InputTokens
	a.stats.OutputTokens += record.OutputTokens
	a.stats.TotalTokens += record.TotalTokens
	if a.stats.ByModel == nil {
		a.stats.ByModel = map[string]int{}
	}
	model := strings.TrimSpace(record.Model)
	if model == "" {
		model = "-"
	}
	if record.TotalTokens > 0 {
		a.stats.ByModel[model] += record.TotalTokens
		if isUsageBearingRequest(record.Path) && model != "-" {
			a.stats.LastModel = model
		}
	}
	a.stats.LastUpdated = time.Now().Format(time.RFC3339)
}

func (a *App) reconcileTokenStatsLocked() {
	if a.stats.ByModel == nil {
		a.stats.ByModel = map[string]int{}
	}
	a.stats.LastModel = ""
	for i := len(a.requests) - 1; i >= 0; i-- {
		record := a.requests[i]
		model := strings.TrimSpace(record.Model)
		if model == "" || record.TotalTokens <= 0 || !isUsageBearingRequest(record.Path) {
			continue
		}
		a.stats.LastModel = model
		break
	}
}

func isUsageBearingRequest(path string) bool {
	switch strings.TrimSpace(path) {
	case "/v1/messages", "/v1/chat/completions", "/v1/completions":
		return true
	default:
		return false
	}
}

func cloneIntMap(src map[string]int) map[string]int {
	out := make(map[string]int, len(src))
	for k, v := range src {
		out[k] = v
	}
	return out
}

func extractTokenUsage(respBody string) (int, int) {
	if strings.TrimSpace(respBody) == "" {
		return 0, 0
	}
	input, output := extractUsageFromJSON(respBody)
	if input != 0 || output != 0 {
		return input, output
	}
	for _, line := range strings.Split(respBody, "\n") {
		line = strings.TrimSpace(line)
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if payload == "" || payload == "[DONE]" {
			continue
		}
		in, out := extractUsageFromJSON(payload)
		if in > 0 {
			input = in
		}
		if out > 0 {
			output = out
		}
	}
	return input, output
}

func extractUsageFromJSON(raw string) (int, int) {
	var payload any
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return 0, 0
	}
	usage, ok := findUsageMap(payload)
	if !ok {
		return 0, 0
	}
	input := intFromAny(usage["input_tokens"]) + intFromAny(usage["prompt_tokens"])
	output := intFromAny(usage["output_tokens"]) + intFromAny(usage["completion_tokens"])
	return input, output
}

func findUsageMap(value any) (map[string]any, bool) {
	switch typed := value.(type) {
	case map[string]any:
		if usage, ok := typed["usage"].(map[string]any); ok {
			return usage, true
		}
		for _, child := range typed {
			if usage, ok := findUsageMap(child); ok {
				return usage, true
			}
		}
	case []any:
		for _, child := range typed {
			if usage, ok := findUsageMap(child); ok {
				return usage, true
			}
		}
	}
	return nil, false
}

func intFromAny(value any) int {
	switch typed := value.(type) {
	case float64:
		return int(typed)
	case int:
		return typed
	case json.Number:
		n, _ := typed.Int64()
		return int(n)
	default:
		return 0
	}
}

func defaultConfig() service.Config {
	cfg := service.Config{
		Host:                  "127.0.0.1",
		Port:                  8095,
		Backend:               service.BackendRemote,
		Transport:             lingmaipc.TransportAuto,
		Cwd:                   defaultCwd(),
		Mode:                  "agent",
		Model:                 "kmodel",
		ShellType:             defaultShellType(),
		SessionMode:           service.SessionModeAuto,
		Timeout:               0,
		RemoteFallbackEnabled: true,
		RemoteFallbackModels:  service.DefaultRemoteFallbackModels(),
	}

	// Try to load config file from multiple locations
	configPaths := configSearchPaths()
	for _, configPath := range configPaths {
		if info, err := os.Stat(configPath); err == nil && !info.IsDir() {
			if data, err := os.ReadFile(configPath); err == nil {
				var fileCfg struct {
					Host                  string   `json:"host"`
					Port                  int      `json:"port"`
					Backend               string   `json:"backend"`
					Transport             string   `json:"transport"`
					Pipe                  string   `json:"pipe"`
					WebSocketURL          string   `json:"websocket_url"`
					RemoteBaseURL         string   `json:"remote_base_url"`
					RemoteAuthFile        string   `json:"remote_auth_file"`
					RemoteVersion         string   `json:"remote_version"`
					Cwd                   string   `json:"cwd"`
					CurrentFilePath       string   `json:"current_file_path"`
					Mode                  string   `json:"mode"`
					Model                 string   `json:"model"`
					ShellType             string   `json:"shell_type"`
					SessionMode           string   `json:"session_mode"`
					TimeoutSeconds        int      `json:"timeout"`
					RemoteFallbackEnabled *bool    `json:"remote_fallback_enabled"`
					RemoteFallbackModels  []string `json:"remote_fallback_models"`
				}
				if err := json.Unmarshal(data, &fileCfg); err == nil {
					if fileCfg.Host != "" {
						cfg.Host = fileCfg.Host
					}
					if fileCfg.Port > 0 {
						cfg.Port = fileCfg.Port
					}
					if fileCfg.Backend != "" {
						cfg.Backend = service.BackendMode(fileCfg.Backend)
					}
					if fileCfg.Transport != "" {
						if t, err := lingmaipc.ParseTransport(fileCfg.Transport); err == nil {
							cfg.Transport = t
						}
					}
					if fileCfg.Pipe != "" {
						cfg.Pipe = fileCfg.Pipe
					}
					if fileCfg.WebSocketURL != "" {
						cfg.WebSocketURL = fileCfg.WebSocketURL
					}
					if fileCfg.RemoteBaseURL != "" {
						cfg.RemoteBaseURL = fileCfg.RemoteBaseURL
					}
					if fileCfg.RemoteAuthFile != "" {
						cfg.RemoteAuthFile = fileCfg.RemoteAuthFile
					}
					if fileCfg.RemoteVersion != "" {
						cfg.RemoteVersion = fileCfg.RemoteVersion
					}
					if fileCfg.Cwd != "" {
						cfg.Cwd = fileCfg.Cwd
					}
					if fileCfg.CurrentFilePath != "" {
						cfg.CurrentFilePath = fileCfg.CurrentFilePath
					}
					if fileCfg.Mode != "" {
						cfg.Mode = fileCfg.Mode
					}
					if fileCfg.Model != "" {
						cfg.Model = fileCfg.Model
					}
					if fileCfg.ShellType != "" {
						cfg.ShellType = fileCfg.ShellType
					}
					if fileCfg.SessionMode != "" {
						cfg.SessionMode = service.SessionMode(fileCfg.SessionMode)
					}
					if fileCfg.TimeoutSeconds >= 0 {
						cfg.Timeout = time.Duration(fileCfg.TimeoutSeconds) * time.Second
					}
					if fileCfg.RemoteFallbackEnabled != nil {
						cfg.RemoteFallbackEnabled = *fileCfg.RemoteFallbackEnabled
					}
					if len(fileCfg.RemoteFallbackModels) > 0 {
						cfg.RemoteFallbackModels = cleanConfigStrings(fileCfg.RemoteFallbackModels)
					}
				}
				break // loaded successfully
			}
		}
	}

	return cfg
}

func backendLabel(backend service.BackendMode) string {
	switch backend {
	case service.BackendRemote:
		return "远端 API"
	default:
		return "IPC 插件"
	}
}

func maskIdentifier(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	runes := []rune(value)
	if len(runes) <= 8 {
		return string(runes[:1]) + "***"
	}
	return string(runes[:4]) + "..." + string(runes[len(runes)-4:])
}

func cleanConfigStrings(values []string) []string {
	out := make([]string, 0, len(values))
	seen := map[string]bool{}
	for _, value := range values {
		item := strings.TrimSpace(value)
		if item == "" || seen[item] {
			continue
		}
		seen[item] = true
		out = append(out, item)
	}
	return out
}

func configSearchPaths() []string {
	var paths []string
	// 1. Executable directory (for dev / portable mode)
	if exe, err := os.Executable(); err == nil {
		paths = append(paths, filepath.Join(filepath.Dir(exe), "lingma-proxy.json"))
		paths = append(paths, filepath.Join(filepath.Dir(exe), "lingma-ipc-proxy.json"))
	}
	// 2. Current working directory
	if wd, err := os.Getwd(); err == nil {
		paths = append(paths, filepath.Join(wd, "lingma-proxy.json"))
		paths = append(paths, filepath.Join(wd, "lingma-ipc-proxy.json"))
	}
	// 3. User home directory
	if home, err := os.UserHomeDir(); err == nil {
		paths = append(paths, filepath.Join(home, "lingma-proxy.json"))
		paths = append(paths, filepath.Join(home, "lingma-ipc-proxy.json"))
		paths = append(paths, filepath.Join(home, ".config", "lingma-proxy", "config.json"))
		paths = append(paths, filepath.Join(home, ".config", "lingma-ipc-proxy", "config.json"))
	}
	return paths
}

func defaultCwd() string {
	// Use the user's home directory as the default working directory
	// so it works out-of-the-box regardless of where the app is launched.
	if home, err := os.UserHomeDir(); err == nil {
		return home
	}
	if wd, err := os.Getwd(); err == nil {
		return wd
	}
	return "."
}

func defaultShellType() string {
	if goruntime.GOOS == "windows" {
		return "powershell"
	}
	return "zsh"
}

func transportFallbackHint() string {
	return "请确认 Lingma 插件已启动并登录；如果自动探测失败，请到设置页手动填写：远端 API 官方默认域名 https://lingma.alibabacloud.com，企业版请填写你的专属域名；macOS WebSocket 示例 ws://127.0.0.1:36510/，Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx，或 Windows WebSocket 示例 ws://127.0.0.1:36510/。"
}

func warmupFallbackHint(backend service.BackendMode) string {
	if backend == service.BackendRemote {
		return "请检查设置页“当前解析结果”里的远端域名是否为官方或企业专属 API 域名；如果出现 OSS/静态资源域名或模型列表 404，请手动填写远端 API 官方默认域名 https://lingma.alibabacloud.com，企业版请填写你的专属域名，并确认登录态未过期。"
	}
	return "请确认 Lingma 插件已启动并登录；如果自动探测失败，请到设置页手动填写：macOS WebSocket 示例 ws://127.0.0.1:36510/，Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx，或 Windows WebSocket 示例 ws://127.0.0.1:36510/。"
}
