package gemini

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

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

// geminiSession manages multi-turn conversations with the Gemini CLI.
// Each Send() launches a new `gemini -p - --output-format stream-json` process
// with --resume for conversation continuity. The prompt is passed via stdin
// (using -p - flag) to preserve newlines in multi-line messages.
type geminiSession struct {
	cmd       string
	extraArgs []string // extra args from cmd, prepended before gemini args
	workDir   string
	model     string
	mode      string
	timeout   time.Duration
	extraEnv  []string
	events    chan core.Event
	chatID    atomic.Value // stores string — Gemini session ID
	ctx       context.Context
	cancel    context.CancelFunc
	wg        sync.WaitGroup
	alive     atomic.Bool

	pendingMsgs []string // buffered assistant messages awaiting classification
}

func newGeminiSession(ctx context.Context, cmd string, extraArgs []string, workDir, model, mode, resumeID string, extraEnv []string, timeout time.Duration) (*geminiSession, error) {
	sessionCtx, cancel := context.WithCancel(ctx)

	gs := &geminiSession{
		cmd:       cmd,
		extraArgs: extraArgs,
		workDir:   workDir,
		model:     model,
		mode:      mode,
		timeout:   timeout,
		extraEnv:  extraEnv,
		events:    make(chan core.Event, 64),
		ctx:       sessionCtx,
		cancel:   cancel,
	}
	gs.alive.Store(true)

	if resumeID != "" && resumeID != core.ContinueSession {
		gs.chatID.Store(resumeID)
	}

	return gs, nil
}

func (gs *geminiSession) Send(prompt string, images []core.ImageAttachment, files []core.FileAttachment) (err error) {
	if !gs.alive.Load() {
		return fmt.Errorf("session is closed")
	}

	// Save images and files into the workspace so Gemini CLI tools can access them.
	attachDir := filepath.Join(gs.workDir, ".cc-connect", "attachments")
	if (len(images) > 0 || len(files) > 0) && os.MkdirAll(attachDir, 0o755) != nil {
		attachDir = os.TempDir()
	}

	var imageRefs []string
	for i, img := range images {
		ext := ".png"
		switch img.MimeType {
		case "image/jpeg":
			ext = ".jpg"
		case "image/gif":
			ext = ".gif"
		case "image/webp":
			ext = ".webp"
		}
		fname := fmt.Sprintf("img_%d_%d%s", time.Now().UnixMilli(), i, ext)
		fpath := filepath.Join(attachDir, fname)
		if err := os.WriteFile(fpath, img.Data, 0o644); err != nil {
			slog.Warn("geminiSession: failed to save image", "error", err)
			continue
		}
		imageRefs = append(imageRefs, fpath)
	}

	var fileRefs []string
	for i, f := range files {
		fname := filepath.Base(f.FileName)
		if fname == "" || fname == "." || fname == ".." {
			fname = fmt.Sprintf("file_%d_%d", time.Now().UnixMilli(), i)
		}
		fpath := filepath.Join(attachDir, fname)
		if err := os.WriteFile(fpath, f.Data, 0o644); err != nil {
			slog.Warn("geminiSession: failed to save file", "error", err)
			continue
		}
		fileRefs = append(fileRefs, fpath)
	}

	chatID := gs.CurrentSessionID()
	isResume := chatID != ""

	args := append(append([]string{}, gs.extraArgs...),
		"--output-format", "stream-json",
	)

	switch gs.mode {
	case "yolo":
		args = append(args, "-y")
	case "auto_edit":
		args = append(args, "--approval-mode", "auto_edit")
	case "plan":
		args = append(args, "--approval-mode", "plan")
	}

	if isResume {
		args = append(args, "--resume", chatID)
	}
	if gs.model != "" {
		args = append(args, "-m", gs.model)
	}

	// Build prompt with explicit file path references so Gemini can find them.
	fullPrompt := prompt
	if len(imageRefs) > 0 {
		if fullPrompt == "" {
			fullPrompt = "Please analyze the attached image(s)."
		}
		fullPrompt += "\n\n[Attached images saved at: " + strings.Join(imageRefs, ", ") + "]"
	}
	if len(fileRefs) > 0 {
		if fullPrompt == "" {
			fullPrompt = "Please analyze the attached file(s)."
		}
		fullPrompt += "\n\n[Attached files saved at: " + strings.Join(fileRefs, ", ") + "]"
	}

	// Pass prompt via stdin instead of -p flag to preserve newlines.
	// The -p flag can truncate at newline characters in some Gemini CLI versions.
	args = append(args, "-p", "-")

	// Add timeout for each turn to prevent hanging processes
	var cancel context.CancelFunc
	var ctx context.Context
	if gs.timeout > 0 {
		ctx, cancel = context.WithTimeout(gs.ctx, gs.timeout)
	} else {
		ctx, cancel = context.WithCancel(gs.ctx)
	}

	// ensure cancel is called on early return errors
	started := false
	defer func() {
		if !started {
			cancel()
		}
	}()

	slog.Debug("geminiSession: launching", "resume", isResume, "args", core.RedactArgs(args))
	cmd := exec.CommandContext(ctx, gs.cmd, args...)
	// Set a short WaitDelay to ensure I/O goroutines don't block for long after the context is done
	cmd.WaitDelay = 1 * time.Second
	cmd.Dir = gs.workDir
	env := os.Environ()
	if len(gs.extraEnv) > 0 {
		env = core.MergeEnv(env, gs.extraEnv)
	}
	cmd.Env = env
	cmd.Stdin = strings.NewReader(fullPrompt)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("geminiSession: stdout pipe: %w", err)
	}

	var stderrBuf bytes.Buffer
	cmd.Stderr = &stderrBuf

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("geminiSession: start: %w", err)
	}

	started = true
	gs.wg.Add(1)
	go func() {
		defer cancel()
		gs.readLoop(ctx, cmd, stdout, &stderrBuf, append(imageRefs, fileRefs...))
	}()

	return nil
}

func (gs *geminiSession) readLoop(ctx context.Context, cmd *exec.Cmd, stdout io.ReadCloser, stderrBuf *bytes.Buffer, tempImages []string) {
	defer gs.wg.Done()
	defer func() {
		// Clean up temp image files
		for _, f := range tempImages {
			os.Remove(f)
		}
		if err := cmd.Wait(); err != nil {
			stderrMsg := strings.TrimSpace(stderrBuf.String())
			if stderrMsg != "" {
				slog.Error("geminiSession: process failed", "error", err, "stderr", stderrMsg)
				evt := core.Event{Type: core.EventError, Error: fmt.Errorf("%s", stderrMsg)}
				select {
				case gs.events <- evt:
				case <-gs.ctx.Done():
					return
				}
			}
		}
	}()

	// Unblock scanner if context is canceled
	go func() {
		<-ctx.Done()
		stdout.Close()
	}()

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

	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			continue
		}

		slog.Debug("geminiSession: raw", "line", truncate(line, 500))

		var raw map[string]any
		if err := json.Unmarshal([]byte(line), &raw); err != nil {
			slog.Debug("geminiSession: non-JSON line", "line", line)
			continue
		}

		gs.handleEvent(raw)
	}

	if err := scanner.Err(); err != nil {
		slog.Error("geminiSession: scanner error", "error", err)
		evt := core.Event{Type: core.EventError, Error: fmt.Errorf("read stdout: %w", err)}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	}
}

// Gemini CLI stream-json event types:
//
//	init       — session_id, model
//	message    — role (user/assistant), content, delta
//	tool_use   — tool_name, tool_id, parameters
//	tool_result — tool_id, status, output, error
//	error      — severity, message
//	result     — status, stats (final event)
func (gs *geminiSession) handleEvent(raw map[string]any) {
	eventType, _ := raw["type"].(string)

	switch eventType {
	case "init":
		gs.handleInit(raw)
	case "message":
		gs.handleMessage(raw)
	case "tool_use":
		gs.handleToolUse(raw)
	case "tool_result":
		gs.handleToolResult(raw)
	case "error":
		gs.handleError(raw)
	case "result":
		gs.handleResult(raw)
	default:
		slog.Debug("geminiSession: unhandled event", "type", eventType)
	}
}

func (gs *geminiSession) handleInit(raw map[string]any) {
	sid, _ := raw["session_id"].(string)
	model, _ := raw["model"].(string)

	if sid != "" {
		gs.chatID.Store(sid)
		slog.Debug("geminiSession: session init", "session_id", sid, "model", model)

		evt := core.Event{Type: core.EventText, SessionID: sid, Content: "", ToolName: model}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	}
}

func (gs *geminiSession) handleMessage(raw map[string]any) {
	role, _ := raw["role"].(string)
	content, _ := raw["content"].(string)

	if role == "user" || content == "" {
		return
	}

	// Delta messages are incremental streaming fragments — emit immediately
	// as EventText so engine's stream preview can update in real time.
	// Non-delta messages (complete text) are buffered for later classification
	// (thinking vs final text) based on what event follows.
	delta, _ := raw["delta"].(bool)
	if delta {
		evt := core.Event{Type: core.EventText, Content: content}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
		}
		return
	}

	gs.pendingMsgs = append(gs.pendingMsgs, content)
}

func (gs *geminiSession) handleToolUse(raw map[string]any) {
	gs.flushPendingAsThinking()

	toolName, _ := raw["tool_name"].(string)
	toolID, _ := raw["tool_id"].(string)
	params, _ := raw["parameters"].(map[string]any)

	input := formatToolParams(toolName, params)

	slog.Debug("geminiSession: tool_use", "tool", toolName, "id", toolID)
	evt := core.Event{Type: core.EventToolUse, ToolName: toolName, ToolInput: input}
	select {
	case gs.events <- evt:
	case <-gs.ctx.Done():
		return
	}
}

func (gs *geminiSession) handleToolResult(raw map[string]any) {
	toolID, _ := raw["tool_id"].(string)
	status, _ := raw["status"].(string)
	output, _ := raw["output"].(string)

	slog.Debug("geminiSession: tool_result", "tool_id", toolID, "status", status)

	if status == "error" {
		errObj, _ := raw["error"].(map[string]any)
		if errObj != nil {
			errMsg, _ := errObj["message"].(string)
			if errMsg != "" {
				output = "Error: " + errMsg
			}
		}
	}

	if output != "" {
		evt := core.Event{Type: core.EventToolResult, ToolName: toolID, Content: truncate(output, 500)}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	}
}

func (gs *geminiSession) handleError(raw map[string]any) {
	severity, _ := raw["severity"].(string)
	message, _ := raw["message"].(string)

	if message != "" {
		slog.Warn("geminiSession: error event", "severity", severity, "message", message)
		evt := core.Event{Type: core.EventError, Error: fmt.Errorf("[%s] %s", severity, message)}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	}
}

func (gs *geminiSession) handleResult(raw map[string]any) {
	gs.flushPendingAsText()

	status, _ := raw["status"].(string)

	var errMsg string
	if status == "error" {
		errObj, _ := raw["error"].(map[string]any)
		if errObj != nil {
			errMsg, _ = errObj["message"].(string)
		}
	}

	sid := gs.CurrentSessionID()

	if errMsg != "" {
		evt := core.Event{Type: core.EventResult, Content: errMsg, SessionID: sid, Done: true, Error: fmt.Errorf("%s", errMsg)}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	} else {
		evt := core.Event{Type: core.EventResult, SessionID: sid, Done: true}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
			return
		}
	}
}

func (gs *geminiSession) flushPendingAsThinking() {
	if len(gs.pendingMsgs) == 0 {
		return
	}
	text := strings.Join(gs.pendingMsgs, "")
	gs.pendingMsgs = gs.pendingMsgs[:0]
	if text != "" {
		evt := core.Event{Type: core.EventThinking, Content: text}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
		}
	}
}

func (gs *geminiSession) flushPendingAsText() {
	if len(gs.pendingMsgs) == 0 {
		return
	}
	text := strings.Join(gs.pendingMsgs, "")
	gs.pendingMsgs = gs.pendingMsgs[:0]
	if text != "" {
		evt := core.Event{Type: core.EventText, Content: text}
		select {
		case gs.events <- evt:
		case <-gs.ctx.Done():
		}
	}
}

// RespondPermission is a no-op — Gemini CLI permissions are handled via -y / --approval-mode flags.
func (gs *geminiSession) RespondPermission(_ string, _ core.PermissionResult) error {
	return nil
}

func (gs *geminiSession) Events() <-chan core.Event {
	return gs.events
}

func (gs *geminiSession) CurrentSessionID() string {
	v, _ := gs.chatID.Load().(string)
	return v
}

func (gs *geminiSession) Alive() bool {
	return gs.alive.Load()
}

func (gs *geminiSession) Close() error {
	gs.alive.Store(false)
	gs.cancel()
	done := make(chan struct{})
	go func() {
		gs.wg.Wait()
		close(done)
	}()
	select {
	case <-done:
		close(gs.events)
	case <-time.After(8 * time.Second):
		slog.Warn("geminiSession: close timed out, abandoning wg.Wait")
	}
	return nil
}

// formatToolParams extracts a human-readable summary from tool parameters.
func formatToolParams(toolName string, params map[string]any) string {
	if params == nil {
		return ""
	}

	switch toolName {
	case "shell", "run_shell_command", "Bash":
		if cmd, ok := params["command"].(string); ok {
			return cmd
		}
	case "write_file", "WriteFile":
		fp, _ := params["file_path"].(string)
		if fp == "" {
			fp, _ = params["path"].(string)
		}
		if fp != "" {
			if content, ok := params["content"].(string); ok && content != "" {
				return "`" + fp + "`\n```\n" + content + "\n```"
			}
			return fp
		}
	case "replace", "ReplaceInFile":
		fp, _ := params["file_path"].(string)
		if fp == "" {
			fp, _ = params["path"].(string)
		}
		if fp != "" {
			old, _ := params["old_string"].(string)
			new_, _ := params["new_string"].(string)
			if old == "" {
				old, _ = params["old_str"].(string)
				new_, _ = params["new_str"].(string)
			}
			if old != "" || new_ != "" {
				diff := computeLineDiff(old, new_)
				return "`" + fp + "`\n```diff\n" + diff + "\n```"
			}
			return fp
		}
	case "read_file", "ReadFile":
		if p, ok := params["file_path"].(string); ok {
			return p
		}
		if p, ok := params["path"].(string); ok {
			return p
		}
	case "list_directory", "ListDirectory":
		if p, ok := params["dir_path"].(string); ok {
			return p
		}
		if p, ok := params["path"].(string); ok {
			return p
		}
		if p, ok := params["directory"].(string); ok {
			return p
		}
	case "web_fetch", "WebFetch":
		if p, ok := params["prompt"].(string); ok {
			return p
		}
		if u, ok := params["url"].(string); ok {
			return u
		}
	case "google_web_search", "GoogleWebSearch":
		if q, ok := params["query"].(string); ok {
			return q
		}
	case "activate_skill":
		if n, ok := params["name"].(string); ok {
			return n
		}
	case "search_code", "SearchCode", "Glob", "glob", "Grep", "grep_search":
		if q, ok := params["query"].(string); ok {
			return q
		}
		if p, ok := params["pattern"].(string); ok {
			return p
		}
	case "save_memory":
		if f, ok := params["fact"].(string); ok {
			return f
		}
	case "ask_user":
		if qs, ok := params["questions"].([]any); ok && len(qs) > 0 {
			if q0, ok := qs[0].(map[string]any); ok {
				if question, ok := q0["question"].(string); ok {
					return question
				}
			}
		}
	case "enter_plan_mode":
		if r, ok := params["reason"].(string); ok {
			return r
		}
	case "exit_plan_mode":
		if p, ok := params["plan_path"].(string); ok {
			return p
		}
	}

	// Fallback: format as key: value pairs for readability
	var parts []string
	for k, v := range params {
		switch val := v.(type) {
		case string:
			parts = append(parts, k+": "+val)
		default:
			j, _ := json.Marshal(val)
			parts = append(parts, k+": "+string(j))
		}
	}
	return strings.Join(parts, ", ")
}

// computeLineDiff computes a minimal unified-style diff between old and new text.
// It finds common prefix/suffix lines and shows only the changed lines with
// up to 1 line of surrounding context. Unchanged context lines are prefixed
// with "  ", removed lines with "- ", and added lines with "+ ".
func computeLineDiff(old, new_ string) string {
	oldLines := strings.Split(old, "\n")
	newLines := strings.Split(new_, "\n")

	// Find common prefix lines
	prefixLen := 0
	minLen := len(oldLines)
	if len(newLines) < minLen {
		minLen = len(newLines)
	}
	for prefixLen < minLen && oldLines[prefixLen] == newLines[prefixLen] {
		prefixLen++
	}

	// Find common suffix lines (not overlapping with prefix)
	suffixLen := 0
	for suffixLen < len(oldLines)-prefixLen && suffixLen < len(newLines)-prefixLen {
		oi := len(oldLines) - 1 - suffixLen
		ni := len(newLines) - 1 - suffixLen
		if oldLines[oi] != newLines[ni] {
			break
		}
		suffixLen++
	}

	// No actual changes
	if prefixLen+suffixLen >= len(oldLines) && prefixLen+suffixLen >= len(newLines) {
		return ""
	}

	// If everything differs (no common lines), show full old/new
	if prefixLen == 0 && suffixLen == 0 {
		var sb strings.Builder
		for _, l := range oldLines {
			sb.WriteString("- " + l + "\n")
		}
		for _, l := range newLines {
			sb.WriteString("+ " + l + "\n")
		}
		return strings.TrimRight(sb.String(), "\n")
	}

	var sb strings.Builder
	const contextN = 1

	// Context: tail of common prefix
	ctxStart := prefixLen - contextN
	if ctxStart < 0 {
		ctxStart = 0
	}
	if ctxStart > 0 {
		sb.WriteString("  ...\n")
	}
	for i := ctxStart; i < prefixLen; i++ {
		sb.WriteString("  " + oldLines[i] + "\n")
	}

	// Removed lines
	for i := prefixLen; i < len(oldLines)-suffixLen; i++ {
		sb.WriteString("- " + oldLines[i] + "\n")
	}

	// Added lines
	for i := prefixLen; i < len(newLines)-suffixLen; i++ {
		sb.WriteString("+ " + newLines[i] + "\n")
	}

	// Context: head of common suffix
	suffStart := len(oldLines) - suffixLen
	suffEnd := suffStart + contextN
	if suffEnd > len(oldLines) {
		suffEnd = len(oldLines)
	}
	for i := suffStart; i < suffEnd; i++ {
		sb.WriteString("  " + oldLines[i] + "\n")
	}
	if suffEnd < len(oldLines) {
		sb.WriteString("  ...")
	}

	return strings.TrimRight(sb.String(), "\n")
}

func truncate(s string, maxRunes int) string {
	if utf8.RuneCountInString(s) <= maxRunes {
		return s
	}
	return string([]rune(s)[:maxRunes]) + "..."
}
