package iflow

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
	"unicode/utf8"

	"github.com/chenhg5/cc-connect/core"
)

func init() {
	core.RegisterAgent("iflow", New)
}

// Agent drives iFlow CLI one turn at a time using interactive `iflow -i`
// inside a PTY, then reconstructs streaming events from the transcript JSONL.
//
// Modes (maps to iFlow CLI flags):
//   - "default":   manual approval mode (--default)
//   - "auto-edit": auto-edit mode (--autoEdit)
//   - "plan":      read-only planning mode (--plan)
//   - "yolo":      auto-approve all tool calls (--yolo)
type Agent struct {
	workDir        string
	model          string
	mode           string
	cmd            string
	cliExtraArgs   []string // extra args from cmd after the binary name
	configEnv      []string // env vars from [projects.agent.options.env]
	toolTimeoutSec int
	providers      []core.ProviderConfig
	activeIdx      int
	sessionEnv     []string
	mu             sync.RWMutex
}

func New(opts map[string]any) (core.Agent, error) {
	workDir, _ := opts["work_dir"].(string)
	if workDir == "" {
		workDir = "."
	}
	model, _ := opts["model"].(string)
	mode, _ := opts["mode"].(string)
	mode = normalizeMode(mode)
	cmd, extraArgs := core.ParseCmdOpts(opts, "iflow")

	if _, err := exec.LookPath(cmd); err != nil {
		return nil, fmt.Errorf("iflow: %q CLI not found in PATH, install with: npm i -g @iflow-ai/iflow-cli", cmd)
	}

	var toolTimeoutSec int
	switch v := opts["tool_timeout_secs"].(type) {
	case int64:
		toolTimeoutSec = int(v)
	case int:
		toolTimeoutSec = v
	case float64:
		toolTimeoutSec = int(v)
	}

	return &Agent{
		workDir:        workDir,
		model:          model,
		mode:           mode,
		cmd:            cmd,
		cliExtraArgs:   extraArgs,
		configEnv:      core.ParseConfigEnv(opts),
		toolTimeoutSec: toolTimeoutSec,
		activeIdx:      -1,
	}, nil
}

func normalizeMode(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "auto-edit", "auto_edit", "autoedit", "edit":
		return "auto-edit"
	case "plan":
		return "plan"
	case "yolo", "force", "auto", "bypasspermissions":
		return "yolo"
	default:
		return "default"
	}
}

func (a *Agent) Name() string { return "iflow" }

func (a *Agent) SetWorkDir(dir string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.workDir = dir
	slog.Info("iflow: work_dir changed", "work_dir", dir)
}

func (a *Agent) GetWorkDir() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.workDir
}

func (a *Agent) SetModel(model string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.model = model
	slog.Info("iflow: model changed", "model", model)
}

func (a *Agent) GetModel() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return core.GetProviderModel(a.providers, a.activeIdx, a.model)
}

func (a *Agent) configuredModels() []core.ModelOption {
	a.mu.RLock()
	defer a.mu.RUnlock()
	return core.GetProviderModels(a.providers, a.activeIdx)
}

func (a *Agent) AvailableModels(_ context.Context) []core.ModelOption {
	if models := a.configuredModels(); len(models) > 0 {
		return models
	}
	return []core.ModelOption{
		{Name: "Qwen3-Coder", Desc: "Qwen3 Coder"},
		{Name: "Kimi-K2.5", Desc: "Kimi K2.5"},
		{Name: "DeepSeek-v3", Desc: "DeepSeek v3"},
	}
}

func (a *Agent) SetSessionEnv(env []string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.sessionEnv = env
}

func (a *Agent) StartSession(ctx context.Context, sessionID string) (core.AgentSession, error) {
	a.mu.Lock()
	model := a.model
	mode := a.mode
	cmd := a.cmd
	extraArgs := append([]string{}, a.cliExtraArgs...)
	workDir := a.workDir
	toolTimeoutSec := a.toolTimeoutSec
	extraEnv := append([]string(nil), a.configEnv...)
	extraEnv = append(extraEnv, a.providerEnvLocked()...)
	extraEnv = append(extraEnv, a.sessionEnv...)
	if a.activeIdx >= 0 && a.activeIdx < len(a.providers) {
		if m := a.providers[a.activeIdx].Model; m != "" {
			model = m
		}
	}
	a.mu.Unlock()

	return newIFlowSession(ctx, cmd, extraArgs, workDir, model, mode, sessionID, extraEnv, toolTimeoutSec)
}

func (a *Agent) ListSessions(_ context.Context) ([]core.AgentSessionInfo, error) {
	a.mu.RLock()
	workDir := a.workDir
	a.mu.RUnlock()
	return listIFlowSessions(workDir)
}

func (a *Agent) DeleteSession(_ context.Context, sessionID string) error {
	if sessionID == "" {
		return fmt.Errorf("session id is required")
	}

	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("iflow: cannot determine home dir: %w", err)
	}

	a.mu.RLock()
	workDir := a.workDir
	a.mu.RUnlock()
	absDir := iflowResolvedWorkDir(workDir)

	candidates := []string{
		filepath.Join(homeDir, ".iflow", "projects", iflowProjectKey(absDir), sessionID+".jsonl"),
	}
	for _, p := range candidates {
		if _, err := os.Stat(p); err == nil {
			return os.Remove(p)
		}
	}

	matches, _ := filepath.Glob(filepath.Join(homeDir, ".iflow", "projects", "*", sessionID+".jsonl"))
	if len(matches) == 0 {
		return fmt.Errorf("session file not found: %s", sessionID)
	}
	return os.Remove(matches[0])
}

func (a *Agent) Stop() error { return nil }

// -- ModeSwitcher --

func (a *Agent) SetMode(mode string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.mode = normalizeMode(mode)
	slog.Info("iflow: mode changed", "mode", a.mode)
}

func (a *Agent) GetMode() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.mode
}

func (a *Agent) PermissionModes() []core.PermissionModeInfo {
	return []core.PermissionModeInfo{
		{Key: "default", Name: "Default", NameZh: "默认", Desc: "Manual approval mode", DescZh: "手动审批模式"},
		{Key: "auto-edit", Name: "Auto Edit", NameZh: "自动编辑", Desc: "Auto-edit mode", DescZh: "自动编辑模式"},
		{Key: "plan", Name: "Plan", NameZh: "规划模式", Desc: "Read-only planning mode", DescZh: "只读规划模式"},
		{Key: "yolo", Name: "YOLO", NameZh: "全自动", Desc: "Auto-approve all tool calls", DescZh: "自动批准所有工具调用"},
	}
}

// -- ContextCompressor --

func (a *Agent) CompressCommand() string { return "/compress" }

// -- MemoryFileProvider --

func (a *Agent) ProjectMemoryFile() string {
	a.mu.RLock()
	workDir := a.workDir
	a.mu.RUnlock()
	absDir, err := filepath.Abs(workDir)
	if err != nil {
		absDir = workDir
	}
	return filepath.Join(absDir, "IFLOW.md")
}

func (a *Agent) GlobalMemoryFile() string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(homeDir, ".iflow", "IFLOW.md")
}

// -- ProviderSwitcher --

func (a *Agent) SetProviders(providers []core.ProviderConfig) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.providers = providers
}

func (a *Agent) SetActiveProvider(name string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	if name == "" {
		a.activeIdx = -1
		slog.Info("iflow: provider cleared")
		return true
	}
	for i, p := range a.providers {
		if p.Name == name {
			a.activeIdx = i
			slog.Info("iflow: provider switched", "provider", name)
			return true
		}
	}
	return false
}

func (a *Agent) GetActiveProvider() *core.ProviderConfig {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.activeIdx < 0 || a.activeIdx >= len(a.providers) {
		return nil
	}
	p := a.providers[a.activeIdx]
	return &p
}

func (a *Agent) ListProviders() []core.ProviderConfig {
	a.mu.Lock()
	defer a.mu.Unlock()
	result := make([]core.ProviderConfig, len(a.providers))
	copy(result, a.providers)
	return result
}

func (a *Agent) providerEnvLocked() []string {
	if a.activeIdx < 0 || a.activeIdx >= len(a.providers) {
		return nil
	}
	p := a.providers[a.activeIdx]
	var env []string
	if p.APIKey != "" {
		env = append(env,
			"IFLOW_API_KEY="+p.APIKey,
			"IFLOW_apiKey="+p.APIKey,
		)
	}
	if p.BaseURL != "" {
		env = append(env,
			"IFLOW_BASE_URL="+p.BaseURL,
			"IFLOW_baseUrl="+p.BaseURL,
		)
	}
	for k, v := range p.Env {
		env = append(env, k+"="+v)
	}
	return env
}

// -- Session listing helpers --

type iflowTranscriptLine struct {
	SessionID string    `json:"sessionId"`
	Type      string    `json:"type"`
	Timestamp time.Time `json:"timestamp"`
	Message   struct {
		Role    string `json:"role"`
		Content any    `json:"content"`
	} `json:"message"`
}

func listIFlowSessions(workDir string) ([]core.AgentSessionInfo, error) {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return nil, fmt.Errorf("iflow: cannot determine home dir: %w", err)
	}

	absDir := iflowResolvedWorkDir(workDir)

	sessionDir := filepath.Join(homeDir, ".iflow", "projects", iflowProjectKey(absDir))
	entries, err := os.ReadDir(sessionDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("iflow: read sessions dir: %w", err)
	}

	var sessions []core.AgentSessionInfo
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !strings.HasPrefix(name, "session-") || !strings.HasSuffix(name, ".jsonl") {
			continue
		}

		path := filepath.Join(sessionDir, name)
		sid, summary, msgCount, modifiedAt := parseIFlowSessionFile(path)
		if sid == "" {
			sid = strings.TrimSuffix(name, ".jsonl")
		}
		if summary == "" {
			summary = sid
			if utf8.RuneCountInString(summary) > 12 {
				summary = string([]rune(summary)[:12]) + "..."
			}
		}
		if modifiedAt.IsZero() {
			if fi, err := e.Info(); err == nil {
				modifiedAt = fi.ModTime()
			}
		}

		sessions = append(sessions, core.AgentSessionInfo{
			ID:           sid,
			Summary:      summary,
			MessageCount: msgCount,
			ModifiedAt:   modifiedAt,
		})
	}

	sort.Slice(sessions, func(i, j int) bool {
		return sessions[i].ModifiedAt.After(sessions[j].ModifiedAt)
	})
	return sessions, nil
}

func parseIFlowSessionFile(path string) (sid, summary string, msgCount int, modifiedAt time.Time) {
	f, err := os.Open(path)
	if err != nil {
		return "", "", 0, time.Time{}
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		msgCount++

		var item iflowTranscriptLine
		if err := json.Unmarshal([]byte(line), &item); err != nil {
			continue
		}
		if sid == "" && item.SessionID != "" {
			sid = item.SessionID
		}
		if !item.Timestamp.IsZero() && item.Timestamp.After(modifiedAt) {
			modifiedAt = item.Timestamp
		}
		if summary == "" && item.Type == "user" {
			if text := extractIFlowContentText(item.Message.Content); text != "" {
				summary = firstNonEmptyLine(text)
				if utf8.RuneCountInString(summary) > 60 {
					summary = string([]rune(summary)[:60]) + "..."
				}
			}
		}
	}

	return sid, summary, msgCount, modifiedAt
}

func extractIFlowContentText(content any) string {
	switch v := content.(type) {
	case string:
		return strings.TrimSpace(v)
	case []any:
		for _, it := range v {
			m, ok := it.(map[string]any)
			if !ok {
				continue
			}
			if t, _ := m["text"].(string); t != "" {
				return strings.TrimSpace(t)
			}
		}
	}
	return ""
}

func firstNonEmptyLine(s string) string {
	for _, line := range strings.Split(s, "\n") {
		line = strings.TrimSpace(line)
		if line != "" {
			return line
		}
	}
	return ""
}

func iflowProjectKey(absDir string) string {
	if absDir == "" {
		return ""
	}
	key := filepath.Clean(absDir)
	key = strings.ReplaceAll(key, "\\", "-")
	key = strings.ReplaceAll(key, "/", "-")
	key = strings.ReplaceAll(key, ":", "-")
	return key
}

func iflowResolvedWorkDir(workDir string) string {
	absDir, err := filepath.Abs(workDir)
	if err != nil {
		absDir = workDir
	}
	if resolved, err := filepath.EvalSymlinks(absDir); err == nil && resolved != "" {
		absDir = resolved
	}
	return absDir
}
