package server

import (
	"fmt"
	"strings"
	"time"

	"gopkg.in/yaml.v2"
)

// Mode represents the operating mode of the audit sidecar.
type Mode string

const (
	// ModeTiDB is the TiDB-specific mode with format switch detection
	// and default tidb-/kong- prefix detection.
	ModeTiDB Mode = "tidb"

	// ModeAgent is the generic agent mode without TiDB-specific features.
	// Uses SubPath configuration for bucket path.
	ModeAgent Mode = "agent"
)

// PushTrigger configures when and how to push logs.
type PushTrigger struct {
	// CheckInterval is the interval between log checks.
	// Supports duration string like "5s", "1m". Default: 5s. Min: 1s.
	CheckInterval time.Duration `yaml:"checkInterval"`

	// CleanInterval is the interval between cleanup operations.
	// Default: 10s. Min: 1s.
	CleanInterval time.Duration `yaml:"cleanInterval"`

	// FilesPerPush limits the number of files to push per cycle.
	// nil or <=0 means use default (3). Cannot be disabled.
	FilesPerPush *int `yaml:"filesPerPush"`

	// MaxSize specifies the file size threshold (in MiB) for triggering rotation.
	// Only applies to agent mode.
	// nil = default (10 MiB), 0 = disabled, max = 10240 MiB (10 GiB)
	MaxSize *int `yaml:"maxSize"`

	// MaxLifetime specifies the max time (in seconds) before triggering rotation.
	// Only applies to agent mode.
	// nil = default (86400s, 24 hours), 0 = disabled
	MaxLifetime *int `yaml:"maxLifetime"`
}

type Config struct {
	LogDir string `yaml:"logDir"`

	// Filename of the audit log file, without path.
	// it is controlled by the `tidb_audit_log` and `tidb_audit_log_format` system variables.
	// If empty, the default is `tidb-audit.log`.
	// NOTE: when using JSON format, there is no need to add the `.json` suffix for this field.
	AuditLogFileName string `yaml:"auditLogFileName"`

	// Filename of the slow log file, without path.
	// If empty, the default is `slowlog`.
	SlowLogFileName string `yaml:"slowLogFileName"`

	// An empty BucketURL will not push and delete the log file.
	// AWS: s3://{bucket}/{path}
	// GCP: gcs://{bucket}/{path}
	// Azure: azblob://{account}.blob.core.windows.net/{container}
	// AliCloud: oss://{bucket}.{endpoint}/{path}
	BucketURL string `yaml:"bucketURL"`
	Region    string `yaml:"region"`

	AWS      *AWS      `yaml:"aws"`
	Azure    *Azure    `yaml:"azure"`
	AliCloud *AliCloud `yaml:"alicloud"`

	// Number of concurrent workers for pushing log files to remote storage.
	// If <= 0, defaults to 4. Only used when pushing multiple files.
	PushConcurrency int `yaml:"pushConcurrency"`

	SlowLogMaxBackups int `yaml:"slowLogMaxBackups"`
	// if disk is cleanOldestLogPercent full, we clean the oldest audit log files directly.
	// Should in  range [0.0, 1.0]
	CleanOldestLogPercent   float64 `yaml:"cleanOldestLogPercent"`
	GracefulStopWaitSeconds int     `yaml:"gracefulStopWaitSeconds"`

	// This DO NOT support reload config now.
	// Zero will not serve metrics
	MetricsPort int `yaml:"metricsPort"`

	// The log producer port.
	// 4000 for TiDB, 9080 for Kong audit plugin
	ProducerPort int `yaml:"producerPort"`

	// RotationStrategy specifies the log rotation method.
	// "rename" (default): Uses lumberjack to rename the original file and create a new one.
	//                     Requires the application to support log file reopening.
	// "copytruncate": Copies the content to a backup file, then truncates the original.
	//                 Suitable for applications that don't support SIGHUP or log reopening.
	RotationStrategy string `yaml:"rotationStrategy"`

	// Mode specifies the operating mode: "tidb" (default) or "agent".
	// - tidb: TiDB-specific mode with format switch detection and default tidb-/kong- prefixes
	// - agent: Generic mode without TiDB-specific features, uses SubPath for bucket path
	Mode string `yaml:"mode"`

	// SubPath specifies the bucket path in agent mode.
	// Example: "primary" will generate path like "s3://bucket/path/primary"
	// If empty in agent mode, defaults to "dataplane-agent".
	// In tidb mode, this field is ignored (uses built-in tidb-/kong- detection with suffix).
	SubPath string `yaml:"subPath"`

	// PushTrigger configures when and how to push logs.
	// If not set, uses default values for backward compatibility.
	PushTrigger *PushTrigger `yaml:"pushTrigger"`
}

type AWS struct {
	RoleARN    string `yaml:"roleARN"`
	ExternalID string `yaml:"externalID"`
}

type Azure struct {
	SASToken string `yaml:"sasToken"`
}

type AliCloud struct {
	RoleARN         string `yaml:"roleARN"`
	ExternalID      string `yaml:"externalID"`
	RoleSessionName string `yaml:"roleSessionName"`
}

func (c *Config) redactedForLog() Config {
	redacted := *c
	if c.Azure != nil {
		azure := *c.Azure
		if azure.SASToken != "" {
			azure.SASToken = "***"
		}
		redacted.Azure = &azure
	}
	return redacted
}

func (c *Config) ToYAML() ([]byte, error) {
	data, err := yaml.Marshal(c)
	return data, err
}

// validateMode validates the Mode configuration value.
func validateMode(mode string) error {
	switch Mode(mode) {
	case ModeTiDB, ModeAgent:
		return nil
	default:
		return fmt.Errorf("invalid mode %q: must be %q or %q", mode, ModeTiDB, ModeAgent)
	}
}

// validateRotationStrategy validates the RotationStrategy configuration value.
func validateRotationStrategy(strategy string) error {
	switch RotationStrategy(strategy) {
	case RotationStrategyRename, RotationStrategyCopyTruncate:
		return nil
	default:
		return fmt.Errorf("invalid rotationStrategy %q: must be %q or %q",
			strategy, RotationStrategyRename, RotationStrategyCopyTruncate)
	}
}

// validatePushTrigger validates the PushTrigger configuration.
func validatePushTrigger(pt *PushTrigger) error {
	if pt == nil {
		return nil // use defaults
	}

	if pt.CheckInterval < 0 {
		return fmt.Errorf("pushTrigger.checkInterval must be non-negative")
	}
	if pt.CheckInterval > 0 && pt.CheckInterval < time.Second {
		return fmt.Errorf("pushTrigger.checkInterval must be >= 1s, got %v", pt.CheckInterval)
	}
	if pt.CleanInterval < 0 {
		return fmt.Errorf("pushTrigger.cleanInterval must be non-negative")
	}
	if pt.CleanInterval > 0 && pt.CleanInterval < time.Second {
		return fmt.Errorf("pushTrigger.cleanInterval must be >= 1s, got %v", pt.CleanInterval)
	}
	if pt.FilesPerPush != nil && *pt.FilesPerPush < 0 {
		return fmt.Errorf("pushTrigger.filesPerPush must be non-negative")
	}
	if pt.MaxSize != nil && *pt.MaxSize < 0 {
		return fmt.Errorf("pushTrigger.maxSize must be non-negative")
	}
	if pt.MaxLifetime != nil && *pt.MaxLifetime < 0 {
		return fmt.Errorf("pushTrigger.maxLifetime must be non-negative")
	}

	return nil
}

// validateSubPath validates the SubPath configuration value.
// Only applies to agent mode. In tidb mode, SubPath is ignored.
func validateSubPath(subPath, mode string) error {
	if Mode(mode) != ModeAgent {
		return nil // SubPath only applies to agent mode
	}

	if subPath == "" {
		return nil // Will use default value
	}

	// Prevent path traversal
	if strings.Contains(subPath, "..") {
		return fmt.Errorf("subPath cannot contain '..' (path traversal): %q", subPath)
	}

	// Prevent absolute paths
	if strings.HasPrefix(subPath, "/") {
		return fmt.Errorf("subPath must be a relative path, got %q", subPath)
	}

	// Check for invalid URL characters
	if strings.ContainsAny(subPath, "?#") {
		return fmt.Errorf("subPath contains invalid URL characters (? or #): %q", subPath)
	}

	return nil
}
