// Copyright 2025.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ui

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

// fieldValidator contains validation rules for SSH configuration fields
type fieldValidator struct {
	Required bool
	Pattern  *regexp.Regexp
	Validate func(string) error
	Message  string
}

// ValidationState tracks validation errors for each field
type ValidationState struct {
	errors map[string]string
	mu     sync.RWMutex
}

// NewValidationState creates a new validation state
func NewValidationState() *ValidationState {
	return &ValidationState{
		errors: make(map[string]string),
	}
}

// SetError sets or clears an error for a field
func (v *ValidationState) SetError(field, errMsg string) {
	v.mu.Lock()
	defer v.mu.Unlock()
	if errMsg == "" {
		delete(v.errors, field)
	} else {
		v.errors[field] = errMsg
	}
}

// GetError gets the error for a specific field
func (v *ValidationState) GetError(field string) string {
	v.mu.RLock()
	defer v.mu.RUnlock()
	return v.errors[field]
}

// HasErrors checks if there are any validation errors
func (v *ValidationState) HasErrors() bool {
	v.mu.RLock()
	defer v.mu.RUnlock()
	return len(v.errors) > 0
}

// GetErrorCount returns the number of validation errors
func (v *ValidationState) GetErrorCount() int {
	v.mu.RLock()
	defer v.mu.RUnlock()
	return len(v.errors)
}

// GetAllErrors returns all validation errors in field order
func (v *ValidationState) GetAllErrors() []string {
	v.mu.RLock()
	defer v.mu.RUnlock()

	// Define field order for consistent error display
	fieldOrder := []string{
		"Alias", "Host", "Port", "User", "Keys", "Tags",
		"ConnectTimeout", "ConnectionAttempts", "ServerAliveInterval", "ServerAliveCountMax",
		"IPQoS", "BindAddress", "LocalForward", "RemoteForward", "DynamicForward",
		"NumberOfPasswordPrompts", "CanonicalizeMaxDots", "EscapeChar",
	}

	// Create a set for O(1) lookups
	fieldOrderSet := make(map[string]bool, len(fieldOrder))
	for _, field := range fieldOrder {
		fieldOrderSet[field] = true
	}

	errors := make([]string, 0, len(v.errors))

	// Add errors in defined order
	for _, field := range fieldOrder {
		if err, exists := v.errors[field]; exists {
			errors = append(errors, fmt.Sprintf("%s: %s", field, err))
		}
	}

	// Add any other errors not in the defined order
	for field, err := range v.errors {
		if !fieldOrderSet[field] {
			errors = append(errors, fmt.Sprintf("%s: %s", field, err))
		}
	}

	return errors
}

// Clear removes all validation errors
func (v *ValidationState) Clear() {
	v.mu.Lock()
	defer v.mu.Unlock()
	v.errors = make(map[string]string)
}

// invalidHostChars contains characters that are not allowed in hostnames
const invalidHostChars = "@#$%^&*()=+[]{}|\\;:'\"<>,?/"

// invalidAddressChars contains characters that are not allowed in bind addresses
const invalidAddressChars = "@#$%^&()=+{}|\\;:'\"<>,?/"

// GetFieldValidators returns validation rules for SSH configuration fields
func GetFieldValidators() map[string]fieldValidator {
	validators := make(map[string]fieldValidator)

	// Basic fields
	validators["Alias"] = fieldValidator{
		Required: true,
		Pattern:  regexp.MustCompile(`^[a-zA-Z0-9._-]+$`),
		Message:  "Alias is required and can only contain letters, numbers, dots, hyphens, and underscores",
	}
	validators["Host"] = fieldValidator{
		Required: true,
		Validate: validateHost,
		Message:  "Host is required and must be a valid hostname or IP address",
	}
	validators["Port"] = fieldValidator{
		Pattern:  regexp.MustCompile(`^([1-9]\d{0,4})$`),
		Validate: validatePort,
		Message:  "Port must be between 1 and 65535",
	}
	validators["User"] = fieldValidator{
		Pattern: regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9._-]*$`),
		Message: "User must start with a letter and contain only letters, numbers, dots, hyphens, and underscores",
	}
	validators["Keys"] = fieldValidator{
		Validate: validateKeyPaths,
		Message:  "Key file not found or not accessible",
	}

	// Connection fields
	validators["ConnectTimeout"] = fieldValidator{
		Validate: validateConnectTimeout,
		Message:  "ConnectTimeout must be a positive number or 'none'",
	}
	validators["ConnectionAttempts"] = fieldValidator{
		Pattern: regexp.MustCompile(`^[1-9]\d*$`),
		Message: "ConnectionAttempts must be a positive number",
	}
	validators["ServerAliveInterval"] = fieldValidator{
		Pattern:  regexp.MustCompile(`^\d+$`),
		Validate: validateNonNegativeNumber,
		Message:  "ServerAliveInterval must be a non-negative number",
	}
	validators["ServerAliveCountMax"] = fieldValidator{
		Pattern:  regexp.MustCompile(`^\d+$`),
		Validate: validateNonNegativeNumber,
		Message:  "ServerAliveCountMax must be a non-negative number",
	}
	validators["IPQoS"] = fieldValidator{
		Validate: validateIPQoS,
		Message:  "IPQoS must be valid QoS values (e.g., 'af21 cs1', 'lowdelay', 'ef')",
	}

	// Address and forwarding fields
	validators["BindAddress"] = fieldValidator{
		Validate: validateBindAddress,
		Message:  "BindAddress must be a valid IP address, hostname, or '*'",
	}
	validators["LocalForward"] = fieldValidator{
		Validate: validatePortForward,
		Message:  "LocalForward must be in format '[bind_address:]port:host:hostport'",
	}
	validators["RemoteForward"] = fieldValidator{
		Validate: validatePortForward,
		Message:  "RemoteForward must be in format '[bind_address:]port:host:hostport'",
	}
	validators["DynamicForward"] = fieldValidator{
		Validate: validateDynamicForward,
		Message:  "DynamicForward must be in format '[bind_address:]port'",
	}

	// Authentication fields
	validators["NumberOfPasswordPrompts"] = fieldValidator{
		Pattern:  regexp.MustCompile(`^\d+$`),
		Validate: validatePasswordPrompts,
		Message:  "NumberOfPasswordPrompts must be between 0 and 10",
	}

	// Advanced fields
	validators["CanonicalizeMaxDots"] = fieldValidator{
		Pattern:  regexp.MustCompile(`^\d+$`),
		Validate: validateNonNegativeNumber,
		Message:  "CanonicalizeMaxDots must be a non-negative number",
	}
	validators["EscapeChar"] = fieldValidator{
		Validate: validateEscapeChar,
		Message:  "EscapeChar must be a single character, 'none', or ^X format (e.g., ^A)",
	}

	// Security fields
	validators["UserKnownHostsFile"] = fieldValidator{
		Validate: validateKnownHostsFiles,
		Message:  "Known hosts file not found or not accessible",
	}

	return validators
}

// validatePort validates port number
func validatePort(value string) error {
	if value == "" {
		return nil // Port is optional
	}
	port, err := strconv.Atoi(value)
	if err != nil {
		return fmt.Errorf("invalid port number")
	}
	if port < 1 || port > 65535 {
		return fmt.Errorf("port must be between 1 and 65535")
	}
	return nil
}

// validateConnectTimeout validates connection timeout
func validateConnectTimeout(value string) error {
	if value == "" || value == "none" {
		return nil
	}
	timeout, err := strconv.Atoi(value)
	if err != nil {
		return fmt.Errorf("invalid timeout value")
	}
	if timeout <= 0 {
		return fmt.Errorf("timeout must be positive or 'none'")
	}
	return nil
}

// validateNonNegativeNumber validates that a value is a non-negative number
func validateNonNegativeNumber(value string) error {
	if value == "" {
		return nil
	}
	num, err := strconv.Atoi(value)
	if err != nil {
		return fmt.Errorf("invalid number")
	}
	if num < 0 {
		return fmt.Errorf("must be non-negative")
	}
	return nil
}

// validatePasswordPrompts validates NumberOfPasswordPrompts
func validatePasswordPrompts(value string) error {
	if value == "" {
		return nil
	}
	num, err := strconv.Atoi(value)
	if err != nil {
		return fmt.Errorf("invalid number")
	}
	if num < 0 || num > 10 {
		return fmt.Errorf("must be between 0 and 10")
	}
	return nil
}

// validateEscapeChar validates escape character format
func validateEscapeChar(value string) error {
	if value == "" || value == "none" || value == "~" {
		return nil
	}
	// Support ^X format (Ctrl+X)
	if len(value) == 2 && value[0] == '^' {
		char := value[1]
		if (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') {
			return nil
		}
	}
	// Single printable character
	if len(value) == 1 && value[0] >= 32 && value[0] <= 126 {
		return nil
	}
	return fmt.Errorf("invalid escape character format")
}

// validateIPQoS validates IPQoS values
func validateIPQoS(value string) error {
	if value == "" {
		return nil
	}
	validValues := map[string]bool{
		"af11": true, "af12": true, "af13": true,
		"af21": true, "af22": true, "af23": true,
		"af31": true, "af32": true, "af33": true,
		"af41": true, "af42": true, "af43": true,
		"cs0": true, "cs1": true, "cs2": true, "cs3": true,
		"cs4": true, "cs5": true, "cs6": true, "cs7": true,
		"ef": true, "le": true,
		"lowdelay": true, "throughput": true, "reliability": true, "none": true,
	}
	// Can be single value or two space-separated values
	parts := strings.Fields(value)
	if len(parts) > 2 {
		return fmt.Errorf("IPQoS accepts at most 2 values")
	}
	for _, part := range parts {
		if !validValues[strings.ToLower(part)] {
			return fmt.Errorf("invalid IPQoS value: %s", part)
		}
	}
	return nil
}

// validateFilePath validates a single file path for existence and readability
func validateFilePath(path string) (exists bool, accessible bool, isDir bool) {
	// Get home directory for tilde expansion
	homeDir, err := os.UserHomeDir()
	if err != nil {
		homeDir = ""
	}

	// Expand tilde notation
	expandedPath := path
	if strings.HasPrefix(path, "~/") && homeDir != "" {
		expandedPath = filepath.Join(homeDir, path[2:])
	} else if strings.HasPrefix(path, "~") && homeDir != "" {
		// Handle ~ alone
		expandedPath = homeDir
	}

	// Check if file exists
	info, err := os.Stat(expandedPath)
	if err != nil {
		if os.IsNotExist(err) {
			return false, false, false // File doesn't exist
		}
		// Permission denied or other error
		return true, false, false // File exists but not accessible
	}

	// Check if it's a directory
	if info.IsDir() {
		return true, true, true
	}

	// Check if file is readable
	// #nosec G304 - expandedPath is validated user input
	file, err := os.Open(expandedPath)
	if err != nil {
		return true, false, false // File exists but not readable
	}
	_ = file.Close()

	return true, true, false // File exists and is readable
}

// buildFileValidationError builds an error message from invalid and inaccessible file paths
func buildFileValidationError(invalidPaths, inaccessiblePaths []string) error {
	var errors []string
	if len(invalidPaths) > 0 {
		errors = append(errors, fmt.Sprintf("file(s) not found: %s", strings.Join(invalidPaths, ", ")))
	}
	if len(inaccessiblePaths) > 0 {
		errors = append(errors, fmt.Sprintf("file(s) not accessible: %s", strings.Join(inaccessiblePaths, ", ")))
	}

	if len(errors) > 0 {
		return fmt.Errorf("%s", strings.Join(errors, "; "))
	}
	return nil
}

// validateFilePaths validates multiple file paths with a custom separator
func validateFilePaths(files string, separator string) error {
	if files == "" {
		return nil
	}
	// Check for invalid characters first, before trimming
	if strings.ContainsAny(files, "\n\r\t") {
		return fmt.Errorf("file path contains invalid characters")
	}

	var paths []string
	if separator == " " {
		// For space separator, use Fields to handle multiple spaces
		paths = strings.Fields(files)
	} else {
		// For other separators like comma
		paths = strings.Split(files, separator)
	}

	var invalidPaths []string
	var inaccessiblePaths []string

	for _, path := range paths {
		path = strings.TrimSpace(path)
		if path == "" {
			continue
		}

		exists, accessible, isDir := validateFilePath(path)

		switch {
		case !exists:
			invalidPaths = append(invalidPaths, path)
		case isDir:
			invalidPaths = append(invalidPaths, fmt.Sprintf("%s (is a directory)", path))
		case !accessible:
			inaccessiblePaths = append(inaccessiblePaths, path)
		}
	}

	return buildFileValidationError(invalidPaths, inaccessiblePaths)
}

// validateKeyPaths validates SSH key file paths (comma-separated)
func validateKeyPaths(keys string) error {
	return validateFilePaths(keys, ",")
}

// validateKnownHostsFiles validates known_hosts file paths (space-separated)
func validateKnownHostsFiles(files string) error {
	// Empty is valid - SSH will use default
	return validateFilePaths(files, " ")
}

// validateHost validates a hostname or IP address
func validateHost(host string) error {
	if host == "" {
		return fmt.Errorf("host is required")
	}

	// Check for spaces
	if strings.Contains(host, " ") {
		return fmt.Errorf("host cannot contain spaces")
	}

	// Try to parse as IP address first
	if net.ParseIP(host) != nil {
		return nil
	}

	// Validate as hostname
	return validateHostname(host)
}

// validateHostname validates a hostname (not IP)
func validateHostname(host string) error {
	if len(host) > 253 {
		return fmt.Errorf("hostname too long")
	}

	// Check for invalid characters using a single check
	if strings.ContainsAny(host, invalidHostChars) {
		return fmt.Errorf("host contains invalid characters")
	}

	// Check hostname format
	if strings.HasPrefix(host, ".") || strings.HasSuffix(host, ".") {
		return fmt.Errorf("hostname cannot start or end with a dot")
	}

	if strings.Contains(host, "..") {
		return fmt.Errorf("hostname cannot contain consecutive dots")
	}

	// Validate each label
	return validateHostLabels(host)
}

// validateHostLabels validates each label in a hostname
func validateHostLabels(host string) error {
	labels := strings.Split(host, ".")
	for _, label := range labels {
		if err := validateHostLabel(label); err != nil {
			return err
		}
	}
	return nil
}

// validateHostLabel validates a single hostname label
func validateHostLabel(label string) error {
	if label == "" {
		return fmt.Errorf("hostname has empty label")
	}
	if len(label) > 63 {
		return fmt.Errorf("hostname label too long")
	}
	if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
		return fmt.Errorf("hostname label cannot start or end with hyphen")
	}
	return nil
}

// validatePortForward validates port forwarding specification
func validatePortForward(forward string) error {
	if forward == "" {
		return nil // Port forwarding is optional
	}

	// Support multiple forwards separated by comma
	forwards := strings.Split(forward, ",")
	for _, fwd := range forwards {
		fwd = strings.TrimSpace(fwd)
		if fwd == "" {
			continue
		}

		// Format: [bind_address:]port:host:hostport
		parts := strings.Split(fwd, ":")
		if len(parts) < 3 || len(parts) > 4 {
			return fmt.Errorf("invalid format, expected [bind_address:]port:host:hostport")
		}

		// Validate ports
		var portIdx, hostPortIdx int
		if len(parts) == 3 {
			// port:host:hostport
			portIdx = 0
			hostPortIdx = 2
		} else {
			// bind_address:port:host:hostport
			portIdx = 1
			hostPortIdx = 3

			// Validate bind address
			if parts[0] != "" && parts[0] != "*" {
				if err := validateBindAddress(parts[0]); err != nil {
					return fmt.Errorf("invalid bind address: %w", err)
				}
			}
		}

		// Validate port numbers
		port, err := strconv.Atoi(parts[portIdx])
		if err != nil || port < 1 || port > 65535 {
			return fmt.Errorf("invalid port number: %s", parts[portIdx])
		}

		hostPort, err := strconv.Atoi(parts[hostPortIdx])
		if err != nil || hostPort < 1 || hostPort > 65535 {
			return fmt.Errorf("invalid host port number: %s", parts[hostPortIdx])
		}
	}

	return nil
}

// validateDynamicForward validates dynamic port forwarding specification
func validateDynamicForward(forward string) error {
	if forward == "" {
		return nil // Dynamic forwarding is optional
	}

	// Support multiple forwards separated by comma
	forwards := strings.Split(forward, ",")
	for _, fwd := range forwards {
		fwd = strings.TrimSpace(fwd)
		if fwd == "" {
			continue
		}

		// Format: [bind_address:]port
		parts := strings.Split(fwd, ":")
		if len(parts) > 2 {
			return fmt.Errorf("invalid format, expected [bind_address:]port")
		}

		var portStr string
		if len(parts) == 1 {
			// Just port
			portStr = parts[0]
		} else {
			// bind_address:port
			if parts[0] != "" && parts[0] != "*" {
				if err := validateBindAddress(parts[0]); err != nil {
					return fmt.Errorf("invalid bind address: %w", err)
				}
			}
			portStr = parts[1]
		}

		// Validate port number
		port, err := strconv.Atoi(portStr)
		if err != nil || port < 1 || port > 65535 {
			return fmt.Errorf("invalid port number: %s", portStr)
		}
	}

	return nil
}

// validateBindAddress validates a bind address (IP, hostname, or *)
func validateBindAddress(address string) error {
	if address == "" || address == "*" {
		return nil // Empty or wildcard is valid
	}

	// Check for spaces
	if strings.Contains(address, " ") {
		return fmt.Errorf("address cannot contain spaces")
	}

	// Try to parse as IP address first (including IPv6)
	if net.ParseIP(address) != nil {
		return nil
	}

	// Validate as hostname with relaxed rules
	return validateBindHostname(address)
}

// isNumericDottedFormat checks if the address looks like an IP address (contains only dots and digits)
func isNumericDottedFormat(address string) bool {
	for _, ch := range address {
		if ch != '.' && (ch < '0' || ch > '9') {
			return false
		}
	}
	return strings.Contains(address, ".")
}

// validateBindHostname validates a hostname for bind address (more permissive than regular hostname)
func validateBindHostname(address string) error {
	// Check for invalid characters using a single check
	if strings.ContainsAny(address, invalidAddressChars) {
		return fmt.Errorf("address contains invalid characters")
	}

	// Check hostname format
	if strings.HasPrefix(address, ".") || strings.HasSuffix(address, ".") {
		return fmt.Errorf("address cannot start or end with a dot")
	}

	if strings.HasPrefix(address, "-") || strings.HasSuffix(address, "-") {
		return fmt.Errorf("address cannot start or end with hyphen")
	}

	// Check for consecutive dots
	if strings.Contains(address, "..") {
		return fmt.Errorf("address cannot contain consecutive dots")
	}

	// If it looks like an IP address (contains only dots and digits), validate it more strictly
	if isNumericDottedFormat(address) {
		// Check if all segments are valid numbers
		segments := strings.Split(address, ".")
		// IPv4 should have exactly 4 segments
		if len(segments) == 4 {
			for _, seg := range segments {
				if seg == "" {
					return fmt.Errorf("invalid IP address format")
				}
				num, err := strconv.Atoi(seg)
				if err != nil || num < 0 || num > 255 {
					return fmt.Errorf("invalid IP address format")
				}
			}
			return nil // Valid IPv4
		}
		// If it's not 4 segments but looks numeric, it's invalid
		return fmt.Errorf("invalid address format")
	}

	// Check each label for hyphens at start/end
	if strings.Contains(address, ".") {
		return validateAddressLabels(address)
	}

	return nil
}

// validateAddressLabels validates labels in a bind address
func validateAddressLabels(address string) error {
	labels := strings.Split(address, ".")
	for _, label := range labels {
		if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
			return fmt.Errorf("address label cannot start or end with hyphen")
		}
	}
	return nil
}

// stripColorTags removes tview color tags from a string
func stripColorTags(s string) string {
	// Remove all tview color tags like [red], [-], [yellow], etc.
	colorTagRegex := regexp.MustCompile(`\[[^\]]*\]`)
	return colorTagRegex.ReplaceAllString(s, "")
}
