package quality

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// Scanner orchestrates the code quality scanning process
type Scanner struct {
	detectors map[string]Detector
	finder    FileFinder
	config    *Config
}

// NewScanner creates a new quality scanner
func NewScanner(config *Config) *Scanner {
	return &Scanner{
		detectors: make(map[string]Detector),
		config:    config,
	}
}

// RegisterDetector registers a detector with the scanner
func (s *Scanner) RegisterDetector(detector Detector) {
	s.detectors[detector.Name()] = detector
}

// SetFileFinder sets the file finder for the scanner
func (s *Scanner) SetFileFinder(finder FileFinder) {
	s.finder = finder
}

// Scan performs a comprehensive quality scan
func (s *Scanner) Scan(ctx context.Context) (*ScanResult, error) {
	start := time.Now()

	log.Printf("Starting quality scan for path: %s", s.config.Path)

	allFindings := make([]Finding, 0)
	filesChecked := 0

	// Determine language if not specified
	language := s.config.Language
	if language == "" {
		language = s.detectLanguage(s.config.Path)
		log.Printf("Auto-detected language: %s", language)
	}

	// Get source files
	files, err := s.getSourceFiles(s.config.Path, language)
	if err != nil {
		return nil, fmt.Errorf("failed to get source files: %w", err)
	}

	filesChecked = len(files)
	log.Printf("Found %d source files to analyze", filesChecked)

	// Run all detectors
	for name, detector := range s.detectors {
		log.Printf("Running detector: %s", name)

		// Skip language-specific detectors for different languages
		if langDetector, ok := detector.(LanguageDetector); ok {
			supported := langDetector.SupportedLanguages()
			if len(supported) > 0 && !contains(supported, language) {
				log.Printf("Skipping detector %s for language %s", name, language)
				continue
			}
		}

		findings, err := s.runDetectorSafely(ctx, detector, name)
		if err != nil {
			log.Printf("Detector %s failed: %v", name, err)
			allFindings = append(allFindings, Finding{
				ID:          fmt.Sprintf("detector_error::%s", name),
				Type:        "detector_error",
				Title:       fmt.Sprintf("Detector failed: %s", name),
				Description: fmt.Sprintf("Detector %s failed during scan: %v", name, err),
				File:        s.config.Path,
				Line:        1,
				Severity:    SeverityT2,
				Score:       0,
				Status:      StatusOpen,
				Metadata: map[string]string{
					"detector": name,
					"error":    err.Error(),
				},
				CreatedAt: time.Now(),
				UpdatedAt: time.Now(),
			})
			continue
		}

		// Filter findings based on exclude patterns
		filtered := s.filterFindings(findings)
		allFindings = append(allFindings, filtered...)

		log.Printf("Detector %s found %d issues", name, len(filtered))
	}

	// Calculate scores
	score, strictScore := s.calculateScores(allFindings)

	duration := time.Since(start)

	result := &ScanResult{
		Findings:     allFindings,
		Score:        score,
		StrictScore:  strictScore,
		FilesChecked: filesChecked,
		Duration:     duration.String(),
		Timestamp:    time.Now(),
	}

	log.Printf("Scan completed in %s: %d findings, score: %d (strict: %d)",
		duration, len(allFindings), score, strictScore)

	return result, nil
}

func (s *Scanner) runDetectorSafely(ctx context.Context, detector Detector, name string) (_ []Finding, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("detector panic in %s: %v", name, r)
		}
	}()
	return detector.Detect(ctx, s.config.Path, s.config)
}

// detectLanguage attempts to auto-detect the project language
func (s *Scanner) detectLanguage(path string) string {
	// Keep auto-detection intentionally conservative until full multi-language
	// scanner behavior is validated in tests.
	if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
		return "go"
	}

	// Default to Go if no markers found
	return "go"
}

// getSourceFiles gets all source files for the given language and path
func (s *Scanner) getSourceFiles(path, language string) ([]string, error) {
	if s.finder != nil {
		return s.finder.FindFiles(path, language)
	}

	// Fallback to basic file extension matching
	extensions := map[string][]string{
		"go":         {".go"},
		"typescript": {".ts", ".tsx"},
		"python":     {".py"},
		"java":       {".java"},
		"rust":       {".rs"},
		"javascript": {".js", ".jsx"},
	}

	langExts, ok := extensions[language]
	if !ok {
		langExts = []string{".go"} // default to Go
	}

	var files []string
	err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() {
			// Skip hidden directories and common exclude dirs
			base := filepath.Base(filePath)
			if filePath != path && (strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor") {
				return filepath.SkipDir
			}
			return nil
		}

		// Check file extension
		ext := filepath.Ext(filePath)
		for _, langExt := range langExts {
			if ext == langExt {
				if !ShouldExclude(filePath, s.config.Exclude) {
					files = append(files, filePath)
				}
				break
			}
		}

		return nil
	})

	return files, err
}

// filterFindings filters findings based on exclude patterns
func (s *Scanner) filterFindings(findings []Finding) []Finding {
	if len(s.config.Exclude) == 0 {
		return findings
	}

	var filtered []Finding
	for _, finding := range findings {
		if !ShouldExclude(finding.File, s.config.Exclude) {
			filtered = append(filtered, finding)
		}
	}

	return filtered
}

// calculateScores calculates quality scores based on findings
func (s *Scanner) calculateScores(findings []Finding) (int, int) {
	totalScore := 0
	strictScore := 0

	for _, finding := range findings {
		weight := int(finding.Severity)
		score := finding.Score * weight
		totalScore += score

		// Strict score includes open and wontfix findings
		if finding.Status == StatusOpen || finding.Status == StatusWontfix {
			strictScore += score
		}
	}

	return totalScore, strictScore
}

// contains checks if a slice contains a string
func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}
