package handlers

import (
	"bufio"
	"context"
	"encoding/json"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Grep caps. These bound the cost of a single grep request to avoid a DoS via a
// crafted pattern / huge tree. All are overridable on the handler.
const (
	DefaultGrepMaxMatches  = 200
	DefaultGrepMaxFiles    = 5000
	DefaultGrepMaxFileSize = 1 << 20 // 1 MiB: skip larger files
	DefaultGrepMaxLineLen  = 1000    // truncate long matched lines
)

// GrepHandler handles fs/grep: a bounded regexp search under the worktree using a
// pure-Go walk (no shell, no git grep interpolation) confined to the pathsafe
// root. Binary files and oversize files are skipped.
type GrepHandler struct {
	MaxMatches  int
	MaxFiles    int
	MaxFileSize int64
}

// NewGrepHandler constructs a GrepHandler with default caps.
func NewGrepHandler() *GrepHandler {
	return &GrepHandler{
		MaxMatches:  DefaultGrepMaxMatches,
		MaxFiles:    DefaultGrepMaxFiles,
		MaxFileSize: DefaultGrepMaxFileSize,
	}
}

func (h *GrepHandler) maxMatches() int {
	if h.MaxMatches > 0 {
		return h.MaxMatches
	}
	return DefaultGrepMaxMatches
}

func (h *GrepHandler) maxFiles() int {
	if h.MaxFiles > 0 {
		return h.MaxFiles
	}
	return DefaultGrepMaxFiles
}

func (h *GrepHandler) maxFileSize() int64 {
	if h.MaxFileSize > 0 {
		return h.MaxFileSize
	}
	return DefaultGrepMaxFileSize
}

type fsGrepParams struct {
	SessionID     string `json:"sessionId"`
	Pattern       string `json:"pattern"`
	Path          string `json:"path,omitempty"` // sub-path to scope the search; defaults to cwd root
	CaseSensitive bool   `json:"case_sensitive,omitempty"`
}

type grepMatch struct {
	File string `json:"file"` // worktree-relative
	Line int    `json:"line"` // 1-based
	Text string `json:"text"`
}

type fsGrepResult struct {
	Matches   []grepMatch `json:"matches"`
	Truncated bool        `json:"truncated"`
}

// Handle runs a regexp search. Returns FsError for path escape (-32001), invalid
// pattern (-32602), or read failure. Match/file caps are honored.
func (h *GrepHandler) Handle(ctx context.Context, sess SessionContext, paramsJSON json.RawMessage) FsResult {
	var p fsGrepParams
	if err := json.Unmarshal(paramsJSON, &p); err != nil {
		return FsResult{Err: &FsError{Code: -32602, Message: "invalid params: " + err.Error()}}
	}
	if strings.TrimSpace(p.Pattern) == "" {
		return FsResult{Err: &FsError{Code: -32602, Message: "pattern required"}}
	}
	expr := p.Pattern
	if !p.CaseSensitive {
		expr = "(?i)" + expr
	}
	re, err := regexp.Compile(expr)
	if err != nil {
		return FsResult{Err: &FsError{Code: -32602, Message: "invalid pattern: " + err.Error()}}
	}

	searchPath := p.Path
	if searchPath == "" {
		searchPath = "."
	}
	root, err := resolveSafePath(sess.CwdPath, searchPath)
	if err != nil {
		return FsResult{Err: &FsError{Code: -32001, Message: err.Error()}}
	}

	cwd := filepath.Clean(sess.CwdPath)
	out := fsGrepResult{Matches: make([]grepMatch, 0, 16)}
	maxMatches := h.maxMatches()
	maxFiles := h.maxFiles()
	maxSize := h.maxFileSize()
	filesScanned := 0

	walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, werr error) error {
		if werr != nil {
			return nil // skip unreadable entries
		}
		select {
		case <-ctx.Done():
			return filepath.SkipAll
		default:
		}
		if d.IsDir() {
			if isSkippedDir(d.Name()) {
				return filepath.SkipDir
			}
			return nil
		}
		if filesScanned >= maxFiles {
			out.Truncated = true
			return filepath.SkipAll
		}
		info, ierr := d.Info()
		if ierr != nil || info.Size() > maxSize {
			return nil
		}
		filesScanned++
		rel, rerr := filepath.Rel(cwd, path)
		if rerr != nil {
			rel = path
		}
		rel = filepath.ToSlash(rel)
		if grepFile(path, rel, re, &out, maxMatches) {
			out.Truncated = true
			return filepath.SkipAll
		}
		return nil
	})
	if walkErr != nil && walkErr != filepath.SkipAll {
		return FsResult{Err: &FsError{Code: -32603, Message: "grep walk: " + walkErr.Error()}}
	}

	res, _ := json.Marshal(out)
	return FsResult{OK: res}
}

// grepFile scans one file line-by-line. Returns true when the global match cap
// was reached (caller should stop the walk).
func grepFile(abs, rel string, re *regexp.Regexp, out *fsGrepResult, maxMatches int) bool {
	f, err := os.Open(abs)
	if err != nil {
		return false
	}
	defer f.Close()

	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
	lineNo := 0
	for sc.Scan() {
		lineNo++
		line := sc.Bytes()
		// Skip binary content (NUL in line).
		if hasNUL(line) {
			return false
		}
		if re.Match(line) {
			text := string(line)
			if len(text) > DefaultGrepMaxLineLen {
				text = text[:DefaultGrepMaxLineLen] + "...[truncated]"
			}
			out.Matches = append(out.Matches, grepMatch{File: rel, Line: lineNo, Text: text})
			if len(out.Matches) >= maxMatches {
				return true
			}
		}
	}
	return false
}

func hasNUL(b []byte) bool {
	for _, c := range b {
		if c == 0 {
			return true
		}
	}
	return false
}

// skippedDirs are never descended into during grep.
var skippedDirs = map[string]struct{}{
	".git": {}, "node_modules": {}, "vendor": {}, "dist": {}, "build": {},
	".venv": {}, "__pycache__": {}, "third_party": {},
}

func isSkippedDir(name string) bool {
	_, ok := skippedDirs[name]
	return ok
}
