package main

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

	lua "github.com/yuin/gopher-lua"
)

const (
	configFileName = "config.lua"
	filesDirName   = "files"
)

type packageConfig struct {
	targets         map[string]string
	disabled        map[string]bool
	ignore          []string
	compiledIgnores []*regexp.Regexp
}

var errTargetDisabled = errors.New("target disabled for this platform")

func loadConfig(path string) (*packageConfig, error) {
	L := lua.NewState()
	defer L.Close()
	L.OpenLibs()

	if err := L.DoFile(path); err != nil {
		return nil, err
	}
	if L.GetTop() == 0 {
		return nil, errors.New("config.lua must return a table")
	}

	value := L.Get(-1)
	tbl, ok := value.(*lua.LTable)
	if !ok {
		return nil, errors.New("config.lua must return a table")
	}

	targetVal := tbl.RawGetString("target")
	targetTbl, ok := targetVal.(*lua.LTable)
	if !ok {
		return nil, errors.New("config.target must be a table")
	}

	targets := make(map[string]string)
	disabled := make(map[string]bool)
	targetTbl.ForEach(func(k, v lua.LValue) {
		ks, ok := k.(lua.LString)
		if !ok {
			return
		}

		if v == lua.LNil || v == lua.LFalse {
			disabled[string(ks)] = true
			return
		}

		vs, ok := v.(lua.LString)
		if !ok {
			return
		}

		targets[string(ks)] = expandHome(string(vs))
	})

	if len(targets) == 0 && len(disabled) == 0 {
		return nil, errors.New("config.target is empty")
	}

	ignore, err := parseIgnore(tbl)
	if err != nil {
		return nil, err
	}
	compiledIgnores, err := compileIgnorePatterns(ignore)
	if err != nil {
		return nil, err
	}

	return &packageConfig{targets: targets, disabled: disabled, ignore: ignore, compiledIgnores: compiledIgnores}, nil
}

func selectTarget(cfg *packageConfig) (string, error) {
	osKey := runtime.GOOS
	if osKey == "darwin" {
		osKey = "macos"
	}
	if cfg.disabled[osKey] {
		return "", errTargetDisabled
	}
	if target, ok := cfg.targets[osKey]; ok {
		return expandHome(target), nil
	}
	if target, ok := cfg.targets["default"]; ok {
		return expandHome(target), nil
	}
	return "", fmt.Errorf("missing target for %s and default", osKey)
}

func parseIgnore(cfgTbl *lua.LTable) ([]string, error) {
	ignoreVal := cfgTbl.RawGetString("ignore")
	if ignoreVal == lua.LNil {
		return nil, nil
	}

	ignoreTbl, ok := ignoreVal.(*lua.LTable)
	if !ok {
		return nil, errors.New("config.ignore must be an array of strings")
	}

	ignore := make([]string, 0, ignoreTbl.Len())
	var parseErr error
	ignoreTbl.ForEach(func(k, v lua.LValue) {
		if parseErr != nil {
			return
		}

		if _, ok := k.(lua.LNumber); !ok {
			parseErr = errors.New("config.ignore must be an array of strings")
			return
		}

		s, ok := v.(lua.LString)
		if !ok {
			parseErr = errors.New("config.ignore must contain only strings")
			return
		}

		pattern := strings.TrimSpace(string(s))
		if pattern == "" {
			parseErr = errors.New("config.ignore cannot contain empty patterns")
			return
		}

		ignore = append(ignore, pattern)
	})
	if parseErr != nil {
		return nil, parseErr
	}
	return ignore, nil
}

func compileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) {
	compiled := make([]*regexp.Regexp, 0, len(patterns))
	for _, pattern := range patterns {
		re, err := globToRegexp(pattern)
		if err != nil {
			return nil, fmt.Errorf("invalid ignore pattern %q: %w", pattern, err)
		}
		compiled = append(compiled, re)
	}
	return compiled, nil
}

func globToRegexp(pattern string) (*regexp.Regexp, error) {
	pattern = strings.ReplaceAll(pattern, "\\", "/")
	pattern = strings.TrimPrefix(pattern, "./")
	if strings.HasPrefix(pattern, "/") {
		pattern = strings.TrimPrefix(pattern, "/")
	}

	var b strings.Builder
	b.WriteString("^")
	for i := 0; i < len(pattern); {
		if i+1 < len(pattern) && pattern[i] == '*' && pattern[i+1] == '*' {
			b.WriteString(".*")
			i += 2
			continue
		}

		ch := pattern[i]
		switch ch {
		case '*':
			b.WriteString("[^/]*")
		case '?':
			b.WriteString("[^/]")
		default:
			b.WriteString(regexp.QuoteMeta(string(ch)))
		}
		i++
	}
	b.WriteString("$")

	return regexp.Compile(b.String())
}

func shouldIgnorePath(rel string, cfg *packageConfig) bool {
	if cfg == nil || len(cfg.compiledIgnores) == 0 {
		return false
	}

	normalized := path.Clean(filepath.ToSlash(rel))
	for _, re := range cfg.compiledIgnores {
		if re.MatchString(normalized) {
			return true
		}
	}

	return false
}

func writeConfig(path, targetRoot string) error {
	osKey := "linux"
	if runtime.GOOS == "darwin" {
		osKey = "macos"
	}

	prettyTarget := compressHome(targetRoot)
	content := fmt.Sprintf("---@class SigilConfig\n---@field target table<string, string|boolean>\n---@field ignore? string[]\n\n---@type SigilConfig\nlocal config = {\n\ttarget = {\n\t\t%s = %q,\n\t\tdefault = %q,\n\t},\n\tignore = {\n\t\t-- \"**/.DS_Store\",\n\t\t-- \"**/*.tmp\",\n\t\t-- \"cache/**\",\n\t},\n}\n\nreturn config\n", osKey, prettyTarget, prettyTarget)
	return os.WriteFile(path, []byte(content), 0o644)
}
