package tools

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/bmatcuk/doublestar/v4"
	"google.golang.org/genai"

	"gokin/internal/logging"
	"gokin/internal/undo"
)

// BatchProgressCallback is called to report progress during batch operations.
type BatchProgressCallback func(processed, total int, currentFile string, success bool)

// BatchTool performs batch operations on multiple files.
type BatchTool struct {
	undoManager      *undo.Manager
	workDir          string
	progressCallback BatchProgressCallback
	failureThreshold float64 // Stop if failure rate exceeds this (0.0 to 1.0, 0 = disabled)
}

// NewBatchTool creates a new BatchTool instance.
func NewBatchTool(workDir string) *BatchTool {
	return &BatchTool{
		workDir: workDir,
	}
}

// SetUndoManager sets the undo manager for tracking changes.
func (t *BatchTool) SetUndoManager(manager *undo.Manager) {
	t.undoManager = manager
}

// SetProgressCallback sets the progress callback for real-time updates.
func (t *BatchTool) SetProgressCallback(callback BatchProgressCallback) {
	t.progressCallback = callback
}

// SetFailureThreshold sets the failure threshold (0.0 to 1.0).
// When the failure rate exceeds this threshold, the batch operation stops.
// Set to 0 to disable (default).
func (t *BatchTool) SetFailureThreshold(threshold float64) {
	if threshold < 0 {
		threshold = 0
	}
	if threshold > 1 {
		threshold = 1
	}
	t.failureThreshold = threshold
}

func (t *BatchTool) Name() string {
	return "batch"
}

func (t *BatchTool) Description() string {
	return "Performs batch operations on multiple files matching a pattern. Supports replace, rename, and delete operations."
}

func (t *BatchTool) Declaration() *genai.FunctionDeclaration {
	return &genai.FunctionDeclaration{
		Name:        t.Name(),
		Description: t.Description(),
		Parameters: &genai.Schema{
			Type: genai.TypeObject,
			Properties: map[string]*genai.Schema{
				"operation": {
					Type:        genai.TypeString,
					Description: "The operation to perform: 'replace', 'rename', 'delete'",
					Enum:        []string{"replace", "rename", "delete"},
				},
				"pattern": {
					Type:        genai.TypeString,
					Description: "Glob pattern to match files (e.g., '**/*.go', 'src/*.ts')",
				},
				"files": {
					Type:        genai.TypeArray,
					Description: "Explicit list of file paths (alternative to pattern)",
					Items:       &genai.Schema{Type: genai.TypeString},
				},
				"search": {
					Type:        genai.TypeString,
					Description: "Text to search for (required for replace operation)",
				},
				"replacement": {
					Type:        genai.TypeString,
					Description: "Replacement text (required for replace operation)",
				},
				"rename_from": {
					Type:        genai.TypeString,
					Description: "Pattern to match in filenames for rename (e.g., '.old')",
				},
				"rename_to": {
					Type:        genai.TypeString,
					Description: "Replacement for filenames (e.g., '.new')",
				},
				"dry_run": {
					Type:        genai.TypeBoolean,
					Description: "Preview changes without applying them (default: false)",
				},
				"parallel": {
					Type:        genai.TypeBoolean,
					Description: "Execute operations in parallel (default: true)",
				},
				"failure_threshold": {
					Type:        genai.TypeNumber,
					Description: "Stop if failure rate exceeds this value (0.0 to 1.0, default: 0 = disabled)",
				},
			},
			Required: []string{"operation"},
		},
	}
}

func (t *BatchTool) Validate(args map[string]any) error {
	op, ok := GetString(args, "operation")
	if !ok || op == "" {
		return NewValidationError("operation", "is required")
	}

	// Check for pattern or files
	pattern, hasPattern := GetString(args, "pattern")
	files, _ := args["files"].([]any)
	hasFiles := len(files) > 0

	if !hasPattern && !hasFiles {
		return NewValidationError("pattern or files", "one of pattern or files is required")
	}

	// Operation-specific validation
	switch op {
	case "replace":
		search, _ := GetString(args, "search")
		if search == "" {
			return NewValidationError("search", "is required for replace operation")
		}
		_, ok := GetString(args, "replacement")
		if !ok {
			return NewValidationError("replacement", "is required for replace operation")
		}

	case "rename":
		from, _ := GetString(args, "rename_from")
		to, _ := GetString(args, "rename_to")
		if from == "" || to == "" {
			return NewValidationError("rename_from/rename_to", "both are required for rename operation")
		}

	case "delete":
		// No additional validation needed

	default:
		return NewValidationError("operation", fmt.Sprintf("unknown operation: %s", op))
	}

	_ = pattern // Used for glob matching
	return nil
}

func (t *BatchTool) Execute(ctx context.Context, args map[string]any) (ToolResult, error) {
	op, _ := GetString(args, "operation")
	pattern, _ := GetString(args, "pattern")
	dryRun := GetBoolDefault(args, "dry_run", false)
	parallel := GetBoolDefault(args, "parallel", true)

	// Collect target files
	var files []string
	var err error

	if pattern != "" {
		files, err = t.matchFiles(pattern)
		if err != nil {
			return NewErrorResult(fmt.Sprintf("pattern error: %s", err)), nil
		}
	}

	// Add explicit files
	if fileList, ok := args["files"].([]any); ok {
		for _, f := range fileList {
			if path, ok := f.(string); ok {
				files = append(files, path)
			}
		}
	}

	if len(files) == 0 {
		return NewErrorResult("no files matched the pattern or list"), nil
	}

	// Execute operation
	var result BatchResult
	switch op {
	case "replace":
		search, _ := GetString(args, "search")
		replacement, _ := GetString(args, "replacement")
		result = t.executeReplace(ctx, files, search, replacement, dryRun, parallel)

	case "rename":
		from, _ := GetString(args, "rename_from")
		to, _ := GetString(args, "rename_to")
		result = t.executeRename(ctx, files, from, to, dryRun, parallel)

	case "delete":
		result = t.executeDelete(ctx, files, dryRun, parallel)

	default:
		return NewErrorResult(fmt.Sprintf("unknown operation: %s", op)), nil
	}

	// Format result
	return t.formatResult(op, result, dryRun), nil
}

// BatchResult holds the results of a batch operation.
type BatchResult struct {
	Succeeded   []string
	Failed      map[string]string // path -> error
	Skipped     []string
	TotalFiles  int
	Description string
}

// matchFiles matches files using glob pattern.
func (t *BatchTool) matchFiles(pattern string) ([]string, error) {
	// Handle relative patterns
	if !filepath.IsAbs(pattern) {
		pattern = filepath.Join(t.workDir, pattern)
	}

	matches, err := doublestar.FilepathGlob(pattern)
	if err != nil {
		return nil, err
	}

	// Filter out directories
	var files []string
	for _, match := range matches {
		info, err := os.Stat(match)
		if err != nil {
			continue
		}
		if !info.IsDir() {
			files = append(files, match)
		}
	}

	return files, nil
}

// executeReplace performs search/replace on multiple files.
func (t *BatchTool) executeReplace(ctx context.Context, files []string, search, replacement string, dryRun, parallel bool) BatchResult {
	result := BatchResult{
		TotalFiles:  len(files),
		Failed:      make(map[string]string),
		Description: fmt.Sprintf("replace '%s' with '%s'", search, replacement),
	}

	if parallel && len(files) > 1 {
		result = t.executeParallel(ctx, files, func(path string) error {
			return t.replaceInFile(path, search, replacement, dryRun)
		})
		result.Description = fmt.Sprintf("replace '%s' with '%s'", search, replacement)
	} else {
		for _, path := range files {
			select {
			case <-ctx.Done():
				result.Failed[path] = "cancelled"
				continue
			default:
			}

			err := t.replaceInFile(path, search, replacement, dryRun)
			if err != nil {
				if strings.Contains(err.Error(), "not found") {
					result.Skipped = append(result.Skipped, path)
				} else {
					result.Failed[path] = err.Error()
				}
			} else {
				result.Succeeded = append(result.Succeeded, path)
			}
		}
	}

	return result
}

// replaceInFile performs search/replace in a single file.
func (t *BatchTool) replaceInFile(path, search, replacement string, dryRun bool) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}

	content := string(data)
	if !strings.Contains(content, search) {
		return fmt.Errorf("search string not found")
	}

	if dryRun {
		return nil
	}

	newContent := strings.ReplaceAll(content, search, replacement)
	newData := []byte(newContent)

	perm := os.FileMode(0644)
	if info, stErr := os.Stat(path); stErr == nil {
		perm = info.Mode().Perm()
	}
	if err := AtomicWrite(path, newData, perm); err != nil {
		return err
	}

	// Record for undo
	if t.undoManager != nil {
		change := undo.NewFileChange(path, "batch_replace", data, newData, false)
		t.undoManager.Record(*change)
	}

	return nil
}

// executeRename renames multiple files.
func (t *BatchTool) executeRename(ctx context.Context, files []string, from, to string, dryRun, parallel bool) BatchResult {
	result := BatchResult{
		TotalFiles:  len(files),
		Failed:      make(map[string]string),
		Description: fmt.Sprintf("rename '%s' to '%s'", from, to),
	}

	// Security: validate rename_to doesn't contain path traversal sequences
	if strings.Contains(to, "..") || strings.Contains(to, "/") || strings.Contains(to, string(filepath.Separator)) {
		result.Failed["validation"] = "rename_to cannot contain path separators or '..'"
		return result
	}

	for _, path := range files {
		select {
		case <-ctx.Done():
			result.Failed[path] = "cancelled"
			continue
		default:
		}

		dir := filepath.Dir(path)
		base := filepath.Base(path)

		if !strings.Contains(base, from) {
			result.Skipped = append(result.Skipped, path)
			continue
		}

		newBase := strings.ReplaceAll(base, from, to)
		newPath := filepath.Join(dir, newBase)

		// Security: verify the new path stays in the same directory
		newPath = filepath.Clean(newPath)
		if filepath.Dir(newPath) != filepath.Clean(dir) {
			result.Failed[path] = "path traversal detected: new path escapes original directory"
			continue
		}

		if dryRun {
			result.Succeeded = append(result.Succeeded, fmt.Sprintf("%s -> %s", path, newPath))
			continue
		}

		if err := os.Rename(path, newPath); err != nil {
			result.Failed[path] = err.Error()
		} else {
			result.Succeeded = append(result.Succeeded, fmt.Sprintf("%s -> %s", path, newPath))

			// Record for undo (file rename)
			if t.undoManager != nil {
				change := undo.NewFileChange(path, "batch_rename", []byte(newPath), nil, false)
				t.undoManager.Record(*change)
			}
		}
	}

	return result
}

// executeDelete deletes multiple files.
func (t *BatchTool) executeDelete(ctx context.Context, files []string, dryRun, parallel bool) BatchResult {
	result := BatchResult{
		TotalFiles:  len(files),
		Failed:      make(map[string]string),
		Description: "delete files",
	}

	for _, path := range files {
		select {
		case <-ctx.Done():
			result.Failed[path] = "cancelled"
			continue
		default:
		}

		// Read content for undo before deletion. Read errors degrade silently
		// (undo just isn't recorded for this file) so a single unreadable file
		// can't abort a batch — but log so the user has a clue why /undo "skips".
		var oldContent []byte
		if !dryRun && t.undoManager != nil {
			var readErr error
			oldContent, readErr = os.ReadFile(path)
			if readErr != nil {
				logging.Warn("batch_delete: undo unavailable, pre-delete read failed",
					"path", path, "error", readErr)
			}
		}

		if dryRun {
			result.Succeeded = append(result.Succeeded, path)
			continue
		}

		if err := os.Remove(path); err != nil {
			result.Failed[path] = err.Error()
		} else {
			result.Succeeded = append(result.Succeeded, path)

			// Record for undo (nil means pre-read failed; []byte{} is a valid empty file).
			if t.undoManager != nil && oldContent != nil {
				change := undo.NewFileChange(path, "batch_delete", oldContent, nil, false)
				t.undoManager.Record(*change)
			}
		}
	}

	return result
}

// executeParallel runs operations in parallel with progress callbacks and failure threshold.
func (t *BatchTool) executeParallel(ctx context.Context, files []string, operation func(string) error) BatchResult {
	result := BatchResult{
		TotalFiles: len(files),
		Failed:     make(map[string]string),
	}

	var wg sync.WaitGroup
	var mu sync.Mutex
	var processedCount int
	var failureCount int
	var shouldStop bool

	// Limit concurrency
	semaphore := make(chan struct{}, 10)

	for _, path := range files {
		// Check if we should stop due to failure threshold
		mu.Lock()
		if shouldStop {
			result.Failed[path] = "stopped due to failure threshold"
			mu.Unlock()
			continue
		}
		mu.Unlock()

		select {
		case <-ctx.Done():
			mu.Lock()
			result.Failed[path] = "cancelled"
			mu.Unlock()
			continue
		default:
		}

		wg.Add(1)
		semaphore <- struct{}{} // Acquire

		go func(p string) {
			defer wg.Done()
			defer func() { <-semaphore }() // Release
			defer func() {
				if rec := recover(); rec != nil {
					logging.Error("batch operation panic",
						"path", p,
						"panic", rec,
						"stack", logging.PanicStack())
					mu.Lock()
					result.Failed[p] = fmt.Sprintf("internal panic: %v", rec)
					mu.Unlock()
				}
			}()

			// Check if context is already cancelled or should stop
			mu.Lock()
			if shouldStop {
				result.Failed[p] = "stopped due to failure threshold"
				mu.Unlock()
				return
			}
			mu.Unlock()

			select {
			case <-ctx.Done():
				mu.Lock()
				result.Failed[p] = "cancelled"
				mu.Unlock()
				return
			default:
			}

			err := operation(p)
			mu.Lock()
			processedCount++
			success := true

			if err != nil {
				success = false
				if strings.Contains(err.Error(), "not found") {
					result.Skipped = append(result.Skipped, p)
				} else {
					result.Failed[p] = err.Error()
					failureCount++

					// Check failure threshold
					if t.failureThreshold > 0 && processedCount >= 3 {
						currentFailureRate := float64(failureCount) / float64(processedCount)
						if currentFailureRate > t.failureThreshold {
							shouldStop = true
							result.Failed["_threshold"] = fmt.Sprintf("failure rate %.1f%% exceeded threshold %.1f%%",
								currentFailureRate*100, t.failureThreshold*100)
						}
					}
				}
			} else {
				result.Succeeded = append(result.Succeeded, p)
			}

			// Call progress callback
			if t.progressCallback != nil {
				t.progressCallback(processedCount, len(files), p, success)
			}

			mu.Unlock()
		}(path)
	}

	wg.Wait()
	return result
}

// formatResult formats the batch result for output.
func (t *BatchTool) formatResult(op string, result BatchResult, dryRun bool) ToolResult {
	var sb strings.Builder

	prefix := ""
	if dryRun {
		prefix = "[DRY RUN] "
	}

	fmt.Fprintf(&sb, "%sBatch %s: %s\n\n", prefix, op, result.Description)

	// Summary
	fmt.Fprintf(&sb, "Total: %d files\n", result.TotalFiles)
	fmt.Fprintf(&sb, "Succeeded: %d\n", len(result.Succeeded))
	if len(result.Skipped) > 0 {
		fmt.Fprintf(&sb, "Skipped: %d\n", len(result.Skipped))
	}
	if len(result.Failed) > 0 {
		fmt.Fprintf(&sb, "Failed: %d\n", len(result.Failed))
	}

	// Details for small result sets
	if len(result.Succeeded) > 0 && len(result.Succeeded) <= 10 {
		sb.WriteString("\nSucceeded:\n")
		for _, path := range result.Succeeded {
			fmt.Fprintf(&sb, "  ✓ %s\n", filepath.Base(path))
		}
	}

	if len(result.Failed) > 0 {
		sb.WriteString("\nFailed:\n")
		for path, err := range result.Failed {
			fmt.Fprintf(&sb, "  ✗ %s: %s\n", filepath.Base(path), err)
		}
	}

	if len(result.Failed) > 0 {
		return NewSuccessResultWithData(sb.String(), map[string]any{
			"succeeded": len(result.Succeeded),
			"failed":    len(result.Failed),
			"skipped":   len(result.Skipped),
			"total":     result.TotalFiles,
		})
	}

	return NewSuccessResult(sb.String())
}
