package commands

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

	"github.com/tanagram/cli/extractor"
	"github.com/tanagram/cli/parser"
	"github.com/tanagram/cli/storage"
)

// Sync manually syncs all instruction files to the cache
func Sync() error {
	// Find git root
	gitRoot, err := storage.FindGitRoot()
	if err != nil {
		return err
	}

	// Find all instruction files
	instructionFiles, err := FindInstructionFiles(gitRoot)
	if err != nil {
		return err
	}

	if len(instructionFiles) == 0 {
		return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md, BUGBOT.md, .cursor/rules/*.mdc)")
	}

	fmt.Printf("Found %d instruction file(s)\n", len(instructionFiles))

	// Load or create cache
	cache, err := storage.LoadCache(gitRoot)
	if err != nil {
		return fmt.Errorf("failed to load cache: %w", err)
	}

	// Check which files have changed and need syncing
	var filesToSync []string
	for _, file := range instructionFiles {
		changed, err := cache.HasChanged(file)
		if err != nil {
			return fmt.Errorf("failed to check if %s changed: %w", file, err)
		}
		if changed {
			filesToSync = append(filesToSync, file)
		}
	}

	// If no files changed, nothing to sync
	if len(filesToSync) == 0 {
		fmt.Println("✓ All instruction files are up to date")
		return nil
	}

	// Parse and sync only changed files using LLM in parallel
	fmt.Printf("\nSyncing policies with LLM (processing %d changed file(s) in parallel)...\n", len(filesToSync))

	ctx := context.Background()

	// Result type for collecting sync results
	type syncResult struct {
		file     string
		relPath  string
		policies []parser.Policy
		err      error
	}

	// Channel to collect results
	results := make(chan syncResult, len(filesToSync))
	var wg sync.WaitGroup

	// Start spinner
	stop := make(chan bool)
	var completed int
	var mu sync.Mutex
	go func() {
		chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
		i := 0
		for {
			select {
			case <-stop:
				fmt.Print("\r")
				return
			default:
				mu.Lock()
				c := completed
				mu.Unlock()
				fmt.Printf("\r%s Processing files... (%d/%d completed)", chars[i%len(chars)], c, len(filesToSync))
				i++
				time.Sleep(100 * time.Millisecond)
			}
		}
	}()

	// Launch goroutines only for changed files
	for _, file := range filesToSync {
		wg.Add(1)
		go func(file string) {
			defer wg.Done()
			relPath, _ := filepath.Rel(gitRoot, file)
			policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
			results <- syncResult{file, relPath, policies, err}
		}(file)
	}

	// Close results channel when all goroutines complete
	go func() {
		wg.Wait()
		close(results)
	}()

	// Collect results
	totalPolicies := 0
	for result := range results {
		if result.err != nil {
			stop <- true
			close(stop)
			time.Sleep(50 * time.Millisecond)
			mu.Lock()
			fmt.Printf("\r\033[K✗ Failed to process %s\n", result.relPath)
			mu.Unlock()
			return fmt.Errorf("failed to extract policies from %s: %w", result.file, result.err)
		}

		if err := cache.UpdateFile(result.file, result.policies); err != nil {
			stop <- true
			close(stop)
			time.Sleep(50 * time.Millisecond)
			return fmt.Errorf("failed to update cache for %s: %w", result.file, err)
		}

		totalPolicies += len(result.policies)

		// Atomic update of counter and output (prevents race with spinner)
		mu.Lock()
		completed++
		fmt.Printf("\r\033[K✓ %s - %d policies\n", result.relPath, len(result.policies))
		mu.Unlock()
	}

	// Stop spinner
	stop <- true
	close(stop)
	time.Sleep(50 * time.Millisecond)

	// Save cache
	if err := cache.Save(); err != nil {
		return fmt.Errorf("failed to save cache: %w", err)
	}

	fmt.Printf("\n✓ Synced %d policies from %d changed file(s)\n", totalPolicies, len(filesToSync))
	return nil
}

// FindInstructionFiles searches for instruction files in the git repository
// Looks for: AGENTS.md, POLICIES.md, CLAUDE.md, BUGBOT.md, and .cursor/rules/*.mdc
func FindInstructionFiles(gitRoot string) ([]string, error) {
	var files []string

	// Common instruction file names to look for
	commonNames := []string{"AGENTS.md", "POLICIES.md", "CLAUDE.md", "BUGBOT.md"}

	// Directories to skip
	skipDirs := map[string]bool{
		".git":              true,
		"node_modules":      true,
		"vendor":            true,
		".venv":             true,
		"venv":              true,
		"__pycache__":       true,
		".pytest_cache":     true,
		".mypy_cache":       true,
		"dist":              true,
		"build":             true,
		".tanagram":         true,
		".conductor":        true, // Skip conductor directories
		"repos":             true, // Skip cloned repos
		"monorepo_clones":   true, // Skip cloned repos
	}

	// Search from git root down
	err := filepath.Walk(gitRoot, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Skip common directories
		if info.IsDir() && skipDirs[info.Name()] {
			return filepath.SkipDir
		}

		// Check if this is one of our instruction files
		if !info.IsDir() {
			// Check for common instruction files (AGENTS.md, etc.)
			for _, name := range commonNames {
				if info.Name() == name {
					files = append(files, path)
					break
				}
			}

			// Check for .mdc files in .cursor/rules/ directory
			if filepath.Ext(info.Name()) == ".mdc" {
				// Check if this file is inside .cursor/rules/
				relPath, err := filepath.Rel(gitRoot, path)
				if err == nil && filepath.HasPrefix(relPath, filepath.Join(".cursor", "rules")) {
					files = append(files, path)
				}
			}
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to search for instruction files: %w", err)
	}

	return files, nil
}
