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"`
	ReqBody    string `json:"reqBody,omitempty"`
	RespBody   string `json:"respBody,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
}

// 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()

	// 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()
}

// 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", "正在停止代理并退出应用")
	if err := a.StopProxy(); err != nil {
		runtime.LogWarningf(a.ctx, "stop proxy before exit failed: %v", err)
	}
	os.Exit(0)
}

func (a *App) emitLog(level string, message string) {
	runtime.EventsEmit(a.ctx, "log", map[string]string{
		"level":   level,
		"message": message,
	})
}

// 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-ipc-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,
	}

	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()
	defer a.mu.Unlock()

	if a.running {
		return fmt.Errorf("proxy already running")
	}

	addr := fmt.Sprintf("%s:%d", a.cfg.Host, a.cfg.Port)
	svc := service.New(a.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("Lingma IPC warmup failed: %v. %s", err, transportFallbackHint()))
	} else {
		runtime.LogInfo(a.ctx, "Lingma IPC warmup completed")
		a.emitLog("info", "Lingma IPC warmup completed")
	}
	cancel()

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

	// 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.server = server
	a.addr = addr
	a.running = true
	a.startedAt = time.Now()

	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
}

// ClearLogs is a no-op backend helper (logs are kept in frontend memory)
func (a *App) ClearLogs() {}

// 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.mu.Unlock()
	a.emitLog("info", "Request history cleared")
}

// 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)
}

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:     120 * time.Second,
	}

	// 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"`
				}
				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
					}
				}
				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 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-ipc-proxy.json"))
	}
	// 2. Current working directory
	if wd, err := os.Getwd(); err == nil {
		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-ipc-proxy.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 插件已启动并登录；如果自动探测失败，请到设置页手动填写：macOS WebSocket 示例 ws://127.0.0.1:36510/，Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx，或 Windows WebSocket 示例 ws://127.0.0.1:36510/。"
}
