package runner

import (
	"bufio"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"sync"

	"github.com/fsnotify/fsnotify"
)

const (
	sliceCmdArgSeparator = ","
	// extWildcard is used in include_ext to match all file extensions
	extWildcard = "*"
)

func (e *Engine) mainLog(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	e.logWithLock(func() {
		e.logger.main()(format, v...)
	})
}

func (e *Engine) mainDebug(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	if e.debugMode {
		e.mainLog(format, v...)
	}
}

func (e *Engine) buildLog(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	if e.debugMode || !e.config.Log.MainOnly {
		e.logWithLock(func() {
			e.logger.build()(format, v...)
		})
	}
}

func (e *Engine) runnerLog(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	if e.debugMode || !e.config.Log.MainOnly {
		e.logWithLock(func() {
			e.logger.runner()(format, v...)
		})
	}
}

func (e *Engine) watcherLog(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	if e.debugMode || !e.config.Log.MainOnly {
		e.logWithLock(func() {
			e.logger.watcher()(format, v...)
		})
	}
}

func (e *Engine) watcherDebug(format string, v ...interface{}) {
	if e.config.Log.Silent {
		return
	}
	if e.debugMode {
		e.watcherLog(format, v...)
	}
}

func (e *Engine) isTmpDir(path string) bool {
	return path == e.config.tmpPath()
}

func (e *Engine) isTestDataDir(path string) bool {
	return path == e.config.testDataPath()
}

func isHiddenDirectory(path string) bool {
	return len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") && filepath.Base(path) != ".."
}

func cleanPath(path string) string {
	return strings.TrimSuffix(strings.TrimSpace(path), "/")
}

func isSubPath(base, target string) bool {
	if base == "" || target == "" {
		return false
	}
	base = filepath.Clean(base)
	target = filepath.Clean(target)
	rel, err := filepath.Rel(base, target)
	if err != nil {
		return false
	}
	if rel == "." {
		return true
	}
	rel = filepath.Clean(rel)
	prefix := ".." + string(os.PathSeparator)
	return rel != ".." && !strings.HasPrefix(rel, prefix)
}

func (e *Engine) isExcludeDir(path string) bool {
	cleanName := cleanPath(e.config.rel(path))
	for _, d := range e.config.Build.ExcludeDir {
		if cleanName == d {
			return true
		}
	}
	return false
}

// return isIncludeDir, walkDir
func (e *Engine) checkIncludeDir(path string) (bool, bool) {
	path = filepath.Clean(path)

	if len(e.config.Build.includeDirAbs) == 0 {
		return true, true
	}

	if !isSubPath(e.config.Root, path) {
		return true, true
	}

	walkDir := false
	for _, dir := range e.config.Build.includeDirAbs {
		dir = filepath.Clean(dir)
		if isSubPath(dir, path) {
			return true, true
		}
		if isSubPath(path, dir) {
			walkDir = true
		}
	}
	return false, walkDir
}

func (e *Engine) checkIncludeFile(path string) bool {
	cleanName := cleanPath(e.config.rel(path))
	iFile := e.config.Build.IncludeFile
	if len(iFile) == 0 { // ignore empty
		return false
	}
	if cleanName == "." {
		return false
	}
	for _, d := range iFile {
		if d == cleanName {
			return true
		}
	}
	return false
}

func (e *Engine) isIncludeExt(path string) bool {
	ext := filepath.Ext(path)
	for _, v := range e.config.Build.IncludeExt {
		if strings.TrimSpace(v) == extWildcard {
			// Wildcard matches all files, but exclude the binary file
			return !e.isBinPath(path)
		}
		if ext == "."+strings.TrimSpace(v) {
			return true
		}
	}
	return false
}

// isBinPath checks if the given path is the binary file path
func (e *Engine) isBinPath(path string) bool {
	binPath := e.config.binPath()
	if binPath == "" {
		return false
	}
	// Normalize the path for comparison
	absPath, err := filepath.Abs(path)
	if err != nil {
		return false
	}
	absBinPath, err := filepath.Abs(binPath)
	if err != nil {
		return false
	}
	return absPath == absBinPath
}

func (e *Engine) isExcludeRegex(path string) (bool, error) {
	regexes, err := e.config.Build.RegexCompiled()
	if err != nil {
		return false, err
	}
	for _, re := range regexes {
		if re.Match([]byte(path)) {
			return true, nil
		}
	}
	return false, nil
}

func (e *Engine) isExcludeFile(path string) bool {
	cleanName := cleanPath(e.config.rel(path))
	for _, d := range e.config.Build.ExcludeFile {
		matched, err := filepath.Match(d, cleanName)
		if err == nil && matched {
			return true
		}
	}
	return false
}

func (e *Engine) writeBuildErrorLog(msg string) error {
	var err error
	f, err := os.OpenFile(e.config.buildLogPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	if _, err = f.Write([]byte(msg)); err != nil {
		return err
	}
	return f.Close()
}

func (e *Engine) withLock(f func()) {
	e.mu.Lock()
	f()
	e.mu.Unlock()
}

func (e *Engine) logWithLock(f func()) {
	e.ll.Lock()
	f()
	e.ll.Unlock()
}

func copyOutput(dst io.Writer, src io.Reader) {
	scanner := bufio.NewScanner(src)
	for scanner.Scan() {
		_, _ = dst.Write([]byte(scanner.Text() + "\n"))
	}
}

func expandPath(path string) (string, error) {
	if strings.HasPrefix(path, "~/") {
		home := os.Getenv("HOME")
		return home + path[1:], nil
	}
	var err error
	wd, err := os.Getwd()
	if err != nil {
		return "", err
	}
	if path == "." {
		return wd, nil
	}
	if strings.HasPrefix(path, "./") {
		return wd + path[1:], nil
	}
	return path, nil
}

func isDir(path string) bool {
	i, err := os.Stat(path)
	if err != nil {
		return false
	}
	return i.IsDir()
}

func validEvent(ev fsnotify.Event) bool {
	return ev.Op&fsnotify.Create == fsnotify.Create ||
		ev.Op&fsnotify.Write == fsnotify.Write ||
		ev.Op&fsnotify.Remove == fsnotify.Remove
}

func removeEvent(ev fsnotify.Event) bool {
	return ev.Op&fsnotify.Remove == fsnotify.Remove
}

func cmdPath(path string) string {
	return strings.Split(path, " ")[0]
}

func adaptToVariousPlatforms(c *Config) {
	// Fix the default configuration is not used in Windows
	// Use the unix configuration on Windows
	if runtime.GOOS == PlatformWindows {
		extName := ".exe"
		originBin := c.Build.Bin

		if 0 < len(c.Build.FullBin) {

			if !strings.HasSuffix(c.Build.FullBin, extName) {
				c.Build.FullBin += extName
			}
		}

		// bin=/tmp/main  cmd=go build -o ./tmp/main.exe main.go
		if !strings.Contains(c.Build.Cmd, c.Build.Bin) && strings.Contains(c.Build.Cmd, originBin) {
			c.Build.Cmd = strings.Replace(c.Build.Cmd, originBin, c.Build.Bin, 1)
		}
	}
}

// fileChecksum returns a checksum for the given file's contents.
func fileChecksum(filename string) (checksum string, err error) {
	contents, err := os.ReadFile(filename)
	if err != nil {
		return "", err
	}

	// If the file is empty, an editor might've been in the process of rewriting the file when we read it.
	// This can happen often if editors are configured to run format after save.
	// Instead of calculating a new checksum, we'll assume the file was unchanged, but return an error to force a rebuild anyway.
	if len(contents) == 0 {
		return "", errors.New("empty file, forcing rebuild without updating checksum")
	}

	h := sha256.New()
	if _, err := h.Write(contents); err != nil {
		return "", err
	}

	return hex.EncodeToString(h.Sum(nil)), nil
}

// checksumMap is a thread-safe map to store file checksums.
type checksumMap struct {
	l sync.Mutex
	m map[string]string
}

// updateFileChecksum updates the filename with the given checksum if different.
func (a *checksumMap) updateFileChecksum(filename, newChecksum string) (ok bool) {
	a.l.Lock()
	defer a.l.Unlock()
	oldChecksum, ok := a.m[filename]
	if !ok || oldChecksum != newChecksum {
		a.m[filename] = newChecksum
		return true
	}
	return false
}

// TomlInfo is a struct for toml config file
type TomlInfo struct {
	fieldPath  string
	field      reflect.StructField
	Value      *string
	fieldValue string
	usage      string
}

func setValue2Struct(v reflect.Value, fieldName string, value string) {
	index := strings.Index(fieldName, ".")
	if index == -1 && len(fieldName) == 0 {
		return
	}
	fields := strings.Split(fieldName, ".")
	var addressableVal reflect.Value
	switch v.Type().String() {
	case "*runner.Config":
		addressableVal = v.Elem()
	default:
		addressableVal = v
	}
	if len(fields) == 1 {
		// string slice int switch case
		field := addressableVal.FieldByName(fieldName)
		switch field.Kind() {
		case reflect.String:
			field.SetString(value)
		case reflect.Slice:
			var parts []string
			if len(value) == 0 {
				parts = []string{}
			} else {
				parts = strings.Split(value, sliceCmdArgSeparator)
			}
			slice := reflect.MakeSlice(field.Type(), len(parts), len(parts))
			for i, part := range parts {
				slice.Index(i).Set(reflect.ValueOf(part))
			}
			field.Set(slice)
		case reflect.Int64:
			i, _ := strconv.ParseInt(value, 10, 64)
			field.SetInt(i)
		case reflect.Int:
			i, _ := strconv.Atoi(value)
			field.SetInt(int64(i))
		case reflect.Bool:
			b, _ := strconv.ParseBool(value)
			field.SetBool(b)
		case reflect.Ptr:
			if field.Type().Elem().Kind() != reflect.String {
				log.Fatalf("unsupported pointer type %s", field.Type().Elem().Kind())
			}
			v := reflect.New(field.Type().Elem())
			v.Elem().SetString(value)
			field.Set(v)
		default:
			log.Fatalf("unsupported type %s", v.FieldByName(fields[0]).Kind())
		}
	} else if len(fields) == 0 {
		return
	} else {
		field := addressableVal.FieldByName(fields[0])
		s2 := fieldName[index+1:]
		setValue2Struct(field, s2, value)
	}
}

// flatConfig ...
func flatConfig(stut interface{}) map[string]TomlInfo {
	m := make(map[string]TomlInfo)
	t := reflect.TypeOf(stut)
	v := reflect.ValueOf(stut)
	setTage2Map("", t, v, m, "")
	return m
}

func getFieldValueString(fieldValue reflect.Value) string {
	switch fieldValue.Kind() {
	case reflect.Slice:
		sliceLen := fieldValue.Len()
		strSlice := make([]string, sliceLen)
		for j := 0; j < sliceLen; j++ {
			strSlice[j] = fmt.Sprintf("%v", fieldValue.Index(j).Interface())
		}
		return strings.Join(strSlice, ",")
	default:
		return fmt.Sprintf("%v", fieldValue.Interface())
	}
}

func setTage2Map(root string, t reflect.Type, v reflect.Value, m map[string]TomlInfo, fieldPath string) {
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fieldValue := v.Field(i)
		tomlVal := field.Tag.Get("toml")

		if field.Type.Kind() == reflect.Struct {
			if field.Anonymous {
				setTage2Map(root, field.Type, fieldValue, m, fieldPath)
				continue
			}
			path := fieldPath + field.Name + "."
			setTage2Map(root+tomlVal+".", field.Type, fieldValue, m, path)
			continue
		}

		if tomlVal == "" {
			continue
		}

		tomlPath := root + tomlVal
		path := fieldPath + field.Name
		var v *string
		str := ""
		v = &str

		fieldValueStr := getFieldValueString(fieldValue)
		usage := field.Tag.Get("usage")
		m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path, fieldValue: fieldValueStr, usage: usage}
	}
}

func joinPath(root, path string) string {
	if filepath.IsAbs(path) {
		return path
	}

	return filepath.Join(root, path)
}

func formatPath(path string) string {
	if !filepath.IsAbs(path) || !strings.Contains(path, " ") {
		return path
	}

	quotedPath := fmt.Sprintf(`"%s"`, path)

	if runtime.GOOS == PlatformWindows {
		return fmt.Sprintf(`& %s`, quotedPath)
	}

	return quotedPath
}

// isDangerousRoot checks if the given path is a dangerous root directory
// that could cause excessive file watching (home dir, root dir, etc.)
// Returns true and a description if the path is dangerous.
func isDangerousRoot(path string) (bool, string) {
	// Get absolute path
	absPath, err := filepath.Abs(path)
	if err != nil {
		return false, ""
	}
	absPath = filepath.Clean(absPath)

	// Check root directory
	if absPath == "/" {
		return true, "root directory (/)"
	}

	// Check home directory
	home, err := os.UserHomeDir()
	if err == nil {
		home = filepath.Clean(home)
		if absPath == home {
			return true, "home directory (~)"
		}
	}

	// Check /root (root user's home, in case UserHomeDir returns something else)
	if absPath == "/root" {
		return true, "/root directory"
	}

	return false, ""
}
