package osvscanner

import (
	"fmt"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"

	"github.com/gobwas/glob"
	"github.com/google/osv-scanner/v2/internal/cachedregexp"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
)

// excludePatterns holds parsed patterns for excluding paths during scanning.
// Supports three types of patterns:
//   - dirsToSkip: exact directory names to skip
//   - globPattern: glob patterns (g:pattern syntax)
//   - regexPattern: regex patterns (r:pattern syntax)
type excludePatterns struct {
	dirsToSkip   []string       // Exact directory names to skip
	globPattern  glob.Glob      // Combined glob pattern using {p1,p2,...} syntax
	regexPattern *regexp.Regexp // Combined regex pattern using (p1|p2|...) syntax
}

// parseExcludePatterns parses the exclude patterns from command line.
// Pattern syntax (matching --lockfile flag style):
//   - "dirname" or ":dirname" -> exact directory name (dirsToSkip)
//   - "g:pattern" -> glob pattern (globPattern)
//   - "r:pattern" -> regex pattern (regexPattern)
//
// The ":" prefix is an escape hatch for directory names containing colons.
func parseExcludePatterns(patterns []string) (*excludePatterns, error) {
	var dirsToSkip []string
	var globPatterns []string
	var regexPatterns []string

	for _, p := range patterns {
		patternType, pattern := parseExcludeArg(p)

		switch patternType {
		case "":
			// Exact directory name
			dirsToSkip = append(dirsToSkip, pattern)
		case "g":
			globPatterns = append(globPatterns, pattern)
		case "r":
			regexPatterns = append(regexPatterns, pattern)
		default:
			return nil, fmt.Errorf("unknown pattern type %q in %q; use g: for glob or r: for regex", patternType, p)
		}
	}

	result := &excludePatterns{
		dirsToSkip: dirsToSkip,
	}

	// Compile glob patterns using {p1,p2,...} syntax
	if len(globPatterns) > 0 {
		var combined string
		if len(globPatterns) == 1 {
			combined = globPatterns[0]
		} else {
			combined = "{" + strings.Join(globPatterns, ",") + "}"
		}
		g, err := glob.Compile(combined, '/')
		if err != nil {
			return nil, fmt.Errorf("invalid glob pattern %q: %w", combined, err)
		}
		result.globPattern = g
	}

	// Compile regex patterns using (p1|p2|...) syntax
	if len(regexPatterns) > 0 {
		var combined string
		if len(regexPatterns) == 1 {
			combined = regexPatterns[0]
		} else {
			combined = "(" + strings.Join(regexPatterns, "|") + ")"
		}
		r, err := cachedregexp.Compile(combined)
		if err != nil {
			return nil, fmt.Errorf("invalid regex pattern %q: %w", combined, err)
		}
		result.regexPattern = r
	}

	return result, nil
}

// parseExcludeArg parses a single exclude argument.
// Returns (patternType, pattern) where:
//   - patternType is "" for exact match, "g" for glob, "r" for regex, or the unknown prefix
//   - pattern is the actual pattern to use
//
// Unknown prefixes are returned as-is so the caller can provide appropriate error messages.
func parseExcludeArg(arg string) (string, string) {
	// Handle Windows absolute paths (e.g., C:\path)
	if runtime.GOOS == "windows" {
		if strings.HasPrefix(arg, "r:") || strings.HasPrefix(arg, "g:") {
			cmdlogger.Warnf("interpreting as regex/glob and not absolute path: %s", arg)
		} else if filepath.IsAbs(arg) {
			return "", arg
		}
	}

	patternType, pattern, found := strings.Cut(arg, ":")
	if !found {
		// No colon found, treat as exact directory name
		return "", arg
	}

	// Empty prefix means exact match (escape hatch for paths with colons)
	// "g" prefix means glob pattern
	// "r" prefix means regex pattern
	// Return all prefixes (including unknown ones) to let the caller handle validation
	return patternType, pattern
}
