/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/
package conf

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime"
	"slices"
	"strconv"
	"strings"
)

// To detect infinite recursion
var recursionCount int

const (
	maxRecursionCount = 10
)

// Constants for valid TLS connection modes
const (
	dbTLSConnectRequired   = "required"
	dbTLSConnectVerifyCA   = "verify_ca"
	dbTLSConnectVerifyFull = "verify_full"
)

// Load reads the configuration file and populates the provided struct
//
// Parameters:
//
//	filename - Config file full path
//	v        - Pointer to struct to be populated (must be addressable)
//
// Struct can contain the following meta tag and parts:
//
//	conf:
//	   optional    - parameter is optional
//	   mandatory   - parameter is mandatory
//	   nonempty    - value must not be empty
//	   range=1:300 - range validation for int values
//	   default=20  - default value
//	   oneof=required verify_ca verify_full
//
// Returns
//
//	error - nil successful, otherwise error
//
// Example
//
//	var options struct {
//	  Host string `conf:"optional"`
//	  Port int `conf:"mandatory,range=1024:55355,default=3306"`
//	  DBTLSConnect  string `conf:"optional,nonempty,oneof=required verify_ca verify_full"`
//	}
//	err := Load("config.conf", &options)
func Load(filename string, v any) error {
	// Get absolute path for the main config file
	absPath, err := filepath.Abs(filename)
	if err != nil {
		return fmt.Errorf("failed to get absolute path: %v", err)
	}

	// Read config files recursively
	configMap, err := readConfigRecursive(absPath, make(map[string]string))
	if err != nil {
		return err
	}

	// Validate and set the struct values
	return setStructValues(v, configMap)
}

// checkGlobPattern checks if a path contains glob pattern characters:
//   - '*' matches any sequence of characters
//   - '?' matches any single character
//   - '[' begins a character class
//   - On Unix: '\' is also treated as special
//
// Parameters:
//
//	path - The file path to check
//
// Returns:
//
//	true if path contains glob patterns, false otherwise
//
// Platform Behavior:
//
//	Windows: Checks for * ? [
//	Unix:    Checks for * ? [ \
//
// Example:
//
//	checkGlobPattern("/etc/*.conf") → true
//	checkGlobPattern("/etc/nginx")  → false
func checkGlobPattern(path string) bool {
	var pattern string

	if runtime.GOOS != "windows" {
		pattern = `*?[\`
	} else {
		pattern = `*?[`
	}

	return strings.ContainsAny(path, pattern)

}

// readConfigRecursive reads config files and handles Include directives
func readConfigRecursive(filename string, existingConfig map[string]string) (map[string]string, error) {
	recursionCount++
	if recursionCount == maxRecursionCount {
		return nil, fmt.Errorf("potential infinite recursion detected")
	}

	// Check if the file exists
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return nil, fmt.Errorf("config file %s does not exist", filename)
	}

	// Read the file
	content, err := os.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to read config file: %v", err)
	}

	// Parse the file into a map
	configMap, err := parseConfigContent(string(content))
	if err != nil {
		return nil, err
	}

	// Merge with existing config (new values override old ones)
	for k, v := range existingConfig {
		if _, exists := configMap[k]; !exists {
			configMap[k] = v
		}
	}

	// Handle Include directive if present
	if includePath, exists := configMap["Include"]; exists {
		delete(configMap, "Include") // Remove the Include directive from final config

		includePath = filepath.Clean(includePath)

		// Handle relative path
		absPath, err := filepath.Abs(includePath)

		if err != nil {
			return nil, fmt.Errorf("failed to get absolute path: %v", err)
		}

		// Pad the relative path with the config directory
		if includePath != absPath {
			confDir := filepath.Dir(filename)
			includePath = filepath.Join(confDir, includePath)
		}

		// Check if the directory contains glob pattern
		if checkGlobPattern(filepath.Dir(includePath)) {
			return nil, fmt.Errorf("directory cannot contain glob pattern: %s", includePath)
		}

		if !checkGlobPattern(includePath) {
			// normal file/directory
			info, err := os.Stat(includePath)

			if err != nil {
				return nil, err
			}

			// Append * at the end of the directory for glob patterns
			if info.IsDir() {
				includePath = filepath.Join(includePath, "*")
			}
		}

		// Handle glob patterns in Include path
		matches, err := filepath.Glob(includePath)
		if err != nil {
			return nil, fmt.Errorf("invalid Include pattern: %v", err)
		}

		// Process each included file
		for _, match := range matches {
			// Skip directories
			if fi, err := os.Stat(match); err == nil && fi.IsDir() {
				continue
			}

			// Recursively process included files
			configMap, err = readConfigRecursive(match, configMap)
			if err != nil {
				return nil, err
			}
		}
	}

	return configMap, nil
}

// parseConfigContent parses the config file content into a map
func parseConfigContent(content string) (map[string]string, error) {
	lines := strings.Split(content, "\n")
	configMap := make(map[string]string)

	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid config line: %s", line)
		}

		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])

		configMap[key] = value
	}

	return configMap, nil
}

// setStructValues sets the struct fields based on the config map and tags
func setStructValues(v any, configMap map[string]string) error {
	val := reflect.ValueOf(v)
	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
		return errors.New("v must be a pointer to a struct")
	}

	val = val.Elem()
	typ := val.Type()

	for i := range val.NumField() {
		field := val.Field(i)
		fieldType := typ.Field(i)

		// Get the conf tag
		tag := fieldType.Tag.Get("conf")
		if tag == "" {
			continue
		}

		// Parse tag options
		options := parseTagOptions(tag)
		configKey := fieldType.Name
		if options.name != "" {
			configKey = options.name
		} else {
			options.name = configKey
		}

		// Get the value from config
		configValue, exists := configMap[configKey]
		if !exists {
			if options.optional {
				if options.defaultValue != "" {
					// Use default value
					configValue = options.defaultValue
				} else {
					// Skip optional field without default
					continue
				}
			} else {
				return fmt.Errorf("missing required config value for %s", configKey)
			}
		}

		// Validate using meta tags and set the value
		if err := validateAndSetValue(field, configValue, options); err != nil {
			return fmt.Errorf("%v", err)
		}

	}

	if err := setDefaultValues(&val, configMap); err != nil {
		return err
	}

	if err := validateHostname(configMap); err != nil {
		return err
	}

	return validateTLSConfig(configMap)

}

// Set default values if they are not provided in the config
func setDefaultValues(val *reflect.Value, configMap map[string]string) error {
	// Handle Hostname
	if configMap["Hostname"] == "" {
		field := val.FieldByName("Hostname")
		if field.IsValid() && field.CanSet() {
			hostname, err := os.Hostname()
			if err != nil {
				return fmt.Errorf("failed to get system hostname: %v", err)
			}
			field.SetString(strings.TrimSpace(hostname))
		}
	}

	// Handle DBPasswordExternal
	if err := assignExternalCommand(val, configMap, "DBPasswordExternal", "DBPassword"); err != nil {
		return err
	}

	// Handle JazDBPasswordExternal
	if err := assignExternalCommand(val, configMap, "JazDBPasswordExternal", "JazDBPassword"); err != nil {
		return err
	}

	return nil
}

// Helper: runs external command and assigns result to struct field
func assignExternalCommand(val *reflect.Value, configMap map[string]string, configKey, fieldName string) error {
	// Check if configKey exists in the map
	if cmdStr, exists := configMap[configKey]; exists {
		cmd := exec.Command("sh", "-c", cmdStr)

		// Execute shell command
		output, err := cmd.Output()
		if err != nil {
			return fmt.Errorf("execute command failure for %s: %v", configKey, err)
		}

		// Assign to struct field
		field := val.FieldByName(fieldName)
		if field.IsValid() && field.CanSet() {
			field.SetString(strings.TrimSpace(string(output)))
		}
	}
	return nil
}

// Validate the TLS parameters in the config file if DBTLSConnect is set
func validateTLSConfig(configMap map[string]string) error {
	/* Handle TLS certificates validations */
	confTLSConnect, exists := configMap["DBTLSConnect"]
	if !exists {
		return nil
	}

	_, hasCa := configMap["DBTLSCAFile"]

	// Check if all three files are defined if 'DBTLSKeyFile' or 'DBTLSCertFile' is defined
	_, hasCert := configMap["DBTLSCertFile"]
	_, hasKey := configMap["DBTLSKeyFile"]
	hasCertOrKey := hasKey || hasCert
	missingRequired := !hasCa || !hasKey || !hasCert

	if hasCertOrKey && missingRequired {
		return fmt.Errorf("parameter 'DBTLSKeyFile' or 'DBTLSCertFile' is defined, but 'DBTLSKeyFile', 'DBTLSCertFile' or 'DBTLSCAFile' is not defined")
	}

	// TLS is enabled
	// Check if CA file is included for verify_ca and verify_full
	if (confTLSConnect == dbTLSConnectVerifyCA || confTLSConnect == dbTLSConnectVerifyFull) && !hasCa {
		return fmt.Errorf("parameter 'DBTLSConnect' value '%s' requires 'DBTLSCAFile', but it is not defined", confTLSConnect)
	}

	return nil
}

var (
	hostnameAllowedRe = regexp.MustCompile(`^[A-Za-z0-9._\- ]+$`)
)

// Validate hostname value
func validateHostname(configMap map[string]string) error {
	const (
		maxHostnameLen = 128
		maxTotalLen    = 2048
	)

	value := configMap["Hostname"]
	if value == "" {
		return nil // empty is allowed
	}

	if len(value) > maxTotalLen {
		return fmt.Errorf(
			"hostname value exceeds maximum length (%d > %d)",
			len(value), maxTotalLen,
		)
	}

	seen := make(map[string]struct{})

	for raw := range strings.SplitSeq(value, ",") {
		host := strings.TrimSpace(raw)

		if host == "" {
			return fmt.Errorf("hostname list contains empty entry")
		}

		if len(host) > maxHostnameLen {
			return fmt.Errorf(
				"hostname '%s' exceeds maximum length (%d > %d)",
				host, len(host), maxHostnameLen,
			)
		}

		if !hostnameAllowedRe.MatchString(host) {
			return fmt.Errorf(
				"hostname '%s' contains invalid characters",
				host,
			)
		}

		if _, exists := seen[host]; exists {
			return fmt.Errorf(
				"duplicate hostname '%s' found",
				host,
			)
		}

		seen[host] = struct{}{}
	}

	return nil
}

// tagOptions represents the parsed conf tag options
type tagOptions struct {
	name         string
	optional     bool
	nonEmpty     bool
	defaultValue string
	rangeMin     *int
	rangeMax     *int
	validValues  []string
}

// parseTagOptions parses the conf tag into options
func parseTagOptions(tag string) tagOptions {
	options := tagOptions{}
	parts := strings.Split(tag, ",")

	for _, part := range parts {
		switch {
		case part == "optional":
			options.optional = true
		case part == "nonempty":
			options.nonEmpty = true
		case strings.HasPrefix(part, "default="):
			options.defaultValue = strings.TrimPrefix(part, "default=")
		case strings.HasPrefix(part, "range="):
			rangeParts := strings.Split(strings.TrimPrefix(part, "range="), ":")
			if len(rangeParts) == 2 {
				min, err := strconv.Atoi(rangeParts[0])
				if err == nil {
					options.rangeMin = &min
				}
				max, err := strconv.Atoi(rangeParts[1])
				if err == nil {
					options.rangeMax = &max
				}
			}
		case strings.HasPrefix(part, "oneof="):
			options.validValues = strings.Split(strings.TrimPrefix(part, "oneof="), " ")
		}

	}

	return options
}

// validateAndSetValue validates and sets the value to the field
func validateAndSetValue(field reflect.Value, value string, options tagOptions) error {
	switch field.Kind() {
	case reflect.String:
		// validations for valid values
		if len(options.validValues) > 0 {
			if !slices.Contains(options.validValues, value) {
				// return fmt.Errorf("invalid value '%s' for %s, must be one of: %v",
				// 	value, options.name, options.validValues)
				return fmt.Errorf("invalid '%s' configuration parameter: '%s'",
					options.name, value)
			}
		}

		// value must not be empty due to nonempty part
		if options.nonEmpty && value == "" {
			return fmt.Errorf("parameter '%s' is defined but empty", options.name)
		}

		field.SetString(value)

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		intValue, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			return fmt.Errorf("invalid integer value: %s", value)
		}

		// Check range if specified
		if options.rangeMin != nil && intValue < int64(*options.rangeMin) {
			return fmt.Errorf("value %d is less than minimum %d", intValue, *options.rangeMin)
		}
		if options.rangeMax != nil && intValue > int64(*options.rangeMax) {
			return fmt.Errorf("value %d is greater than maximum %d", intValue, *options.rangeMax)
		}

		field.SetInt(intValue)
	default:
		return fmt.Errorf("unsupported field type: %s", field.Kind())
	}

	return nil
}
