package configcheck

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/api/healthcheck"
	"github.com/pingcap-inc/clinic/pkg/config"
)

// ConfigChangeRecord represents a configuration change record
type ConfigChangeRecord struct {
	ID                 string         `json:"change_id"`
	ClusterID          string         `json:"cluster_id"`
	CreatedAt          string         `json:"created_at"`
	Reason             string         `json:"reason"`
	Applicant          string         `json:"applicant"`
	ApplyPolicy        string         `json:"apply_policy"`
	AppliedAt          string         `json:"applied_at"`
	ChangeType         string         `json:"change_type"`
	Filename           string         `json:"filename"`
	AllParameters      string         `json:"all_parameters"`
	ModifiedParameters string         `json:"modified_parameters"`
	PreviousText       string         `json:"previous_text"`
	TemplateID         string         `json:"template_id"`
	ModifiedTemplate   sql.NullString `json:"modified_template"`
}

// ParameterChange represents a single parameter change
type ParameterChange struct {
	Name      string `json:"name"`
	From      string `json:"from"`
	To        string `json:"to"`
	RiskLevel string `json:"risk_level,omitempty"`
	Component string `json:"component,omitempty"`
}

// ConfigCheckResult represents the result of config check
type ConfigCheckResult struct {
	ClusterID      string               `json:"cluster_id"`
	ReportID       string               `json:"report_id"`
	CheckTime      time.Time            `json:"check_time"`
	TimeRangeStart string               `json:"time_range_start"`
	TimeRangeEnd   string               `json:"time_range_end"`
	TotalChanges   int                  `json:"total_changes"`
	Changes        []ConfigChangeDetail `json:"changes"`
}

// ConfigChangeDetail represents detailed config change information
type ConfigChangeDetail struct {
	ChangeID         string            `json:"change_id"`
	CreatedAt        string            `json:"created_at"`
	AppliedAt        string            `json:"applied_at"`
	Reason           string            `json:"reason"`
	Applicant        string            `json:"applicant"`
	ApplyPolicy      string            `json:"apply_policy"`
	ChangeType       string            `json:"change_type"`
	Filename         string            `json:"filename"`
	Component        string            `json:"component"`
	ParameterChanges []ParameterChange `json:"parameter_changes"`
}

// DBConfig holds database configuration
type DBConfig struct {
	Host     string
	Port     int
	User     string
	Password string
	Database string
}

// AsyncRun performs async config check for a specific cluster
func AsyncRun(req *healthcheck.HealthCheckJobRequest, opsDBCfg *config.OpsDBConfig) error {
	log.Info("start config check",
		zap.String("cluster_id", req.ClusterId),
		zap.String("report_id", req.ReportId),
		zap.String("time_range_start", req.TimeRange.Start),
		zap.String("time_range_end", req.TimeRange.End),
	)

	// Parse time range from request
	startTime, endTime, err := parseTimeRange(req.TimeRange)
	if err != nil {
		log.Error("parse time range failed", zap.Error(err))
		return fmt.Errorf("parse time range failed: %w", err)
	}

	// Get database configuration from environment
	dbConfig, err := getDBConfig(opsDBCfg)
	if err != nil {
		log.Error("get db config failed", zap.Error(err))
		return fmt.Errorf("get db config failed: %w", err)
	}

	// Query config changes from database
	records, err := queryConfigChanges(dbConfig, req.ClusterId, startTime, endTime)
	if err != nil {
		log.Error("query config changes failed", zap.Error(err))
		return fmt.Errorf("query config changes failed: %w", err)
	}

	// Process records and build result
	result := buildCheckResult(req.ClusterId, req.ReportId, req.TimeRange.Start, req.TimeRange.End, records)

	// Upload result to S3
	s3Key := fmt.Sprintf("healthcheck-report/%s_%s_config_check.json", req.ClusterId, req.ReportId)
	if err := uploadToS3(s3Key, result); err != nil {
		log.Error("upload result to s3 failed", zap.Error(err), zap.String("s3_key", s3Key))
		return fmt.Errorf("upload result to s3 failed: %w", err)
	}

	log.Info("config check completed",
		zap.String("cluster_id", req.ClusterId),
		zap.String("report_id", req.ReportId),
		zap.Int("total_changes", result.TotalChanges),
		zap.String("s3_key", s3Key),
	)

	return nil
}

// parseTimeRange parses time range from request
func parseTimeRange(tr healthcheck.HealthCheckTimeRange) (string, string, error) {
	// Parse and validate time format
	start, err := time.Parse(time.RFC3339, tr.Start)
	if err != nil {
		// Try alternative format
		start, err = time.Parse("2006-01-02 15:04:05", tr.Start)
		if err != nil {
			return "", "", fmt.Errorf("invalid start time format: %w", err)
		}
	}

	end, err := time.Parse(time.RFC3339, tr.End)
	if err != nil {
		// Try alternative format
		end, err = time.Parse("2006-01-02 15:04:05", tr.End)
		if err != nil {
			return "", "", fmt.Errorf("invalid end time format: %w", err)
		}
	}

	// Format for SQL query
	startStr := start.Format("2006-01-02 15:04:05")
	endStr := end.Format("2006-01-02 15:04:05")

	return startStr, endStr, nil
}

// getDBConfig gets database configuration from origin config
func getDBConfig(opsDBCfg *config.OpsDBConfig) (*DBConfig, error) {
	user, err := aesDecryptWithSalt(opsDBCfg.UserEnc, opsDBCfg.AESKey, opsDBCfg.Salt)
	if err != nil {
		return nil, err
	}
	password, err := aesDecryptWithSalt(opsDBCfg.PasswordEnc, opsDBCfg.AESKey, opsDBCfg.Salt)
	if err != nil {
		return nil, err
	}
	return &DBConfig{
		Host:     opsDBCfg.Host,
		Port:     opsDBCfg.Port,
		Database: opsDBCfg.DBName,
		User:     user,
		Password: password,
	}, nil
}

func aesDecryptWithSalt(cipherTextBase64, keyBase64, salt string) (string, error) {
	cipherText, err := base64.StdEncoding.DecodeString(cipherTextBase64)
	if err != nil {
		return "", err
	}
	key, err := base64.StdEncoding.DecodeString(keyBase64)
	if err != nil {
		return "", err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	if len(cipherText) < aes.BlockSize {
		return "", fmt.Errorf("invalid cipher")
	}
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(cipherText, cipherText)
	padLen := int(cipherText[len(cipherText)-1])
	plainText := cipherText[:len(cipherText)-padLen]
	plain := string(plainText)
	if salt != "" && len(plain) > len(salt) && plain[:len(salt)] == salt {
		plain = plain[len(salt):]
	}
	return plain, nil
}

// queryConfigChanges queries config changes from database for specific cluster
func queryConfigChanges(cfg *DBConfig, clusterID, startTime, endTime string) ([]ConfigChangeRecord, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return nil, fmt.Errorf("connect to database failed: %w", err)
	}

	sqlDB, err := db.DB()
	if err != nil {
		return nil, fmt.Errorf("get sql db failed: %w", err)
	}
	defer func() {
		if err := sqlDB.Close(); err != nil {
			log.Warn("close sql db failed", zap.Error(err))
		}
	}()

	// Query config changes for specific cluster and time range
	query := `
		SELECT
			a.change_id AS ID,
			a.cluster_id AS ClusterID,
			b.created_at AS CreatedAt,
			b.reason AS Reason,
			b.applicant AS Applicant,
			b.apply_policy AS ApplyPolicy,
			b.applied_at AS AppliedAt,
			b.change_type AS ChangeType,
			c.filename AS Filename,
			c.all_parameters AS AllParameters,
			c.modified_parameters AS ModifiedParameters,
			c.previous_text AS PreviousText,
			c.template_id AS TemplateID,
			c.modified_template AS ModifiedTemplate
		FROM
			cloudconfig_configuration_change_to_cluster AS a
		LEFT JOIN cloudconfig_configuration_changes AS b ON
			a.change_id = b.id
		LEFT JOIN cloudconfig_configuration_file_changes AS c ON
			c.change_id = a.change_id
		WHERE
			b.phase = 'applied'
			AND a.cluster_id = ?
			AND b.applied_at >= ? AND b.applied_at < ?
		ORDER BY
			AppliedAt DESC
	`

	rows, err := db.Raw(query, clusterID, startTime, endTime).Rows()
	if err != nil {
		return nil, fmt.Errorf("execute query failed: %w", err)
	}
	defer func() {
		if err := rows.Close(); err != nil {
			log.Warn("close rows failed", zap.Error(err))
		}
	}()

	var records []ConfigChangeRecord
	for rows.Next() {
		var record ConfigChangeRecord
		if err := rows.Scan(
			&record.ID,
			&record.ClusterID,
			&record.CreatedAt,
			&record.Reason,
			&record.Applicant,
			&record.ApplyPolicy,
			&record.AppliedAt,
			&record.ChangeType,
			&record.Filename,
			&record.AllParameters,
			&record.ModifiedParameters,
			&record.PreviousText,
			&record.TemplateID,
			&record.ModifiedTemplate,
		); err != nil {
			log.Warn("scan row failed", zap.Error(err))
			continue
		}
		records = append(records, record)
	}

	return records, nil
}

// buildCheckResult builds config check result from records
func buildCheckResult(clusterID, reportID, timeRangeStart, timeRangeEnd string, records []ConfigChangeRecord) *ConfigCheckResult {
	result := &ConfigCheckResult{
		ClusterID:      clusterID,
		ReportID:       reportID,
		CheckTime:      time.Now(),
		TimeRangeStart: timeRangeStart,
		TimeRangeEnd:   timeRangeEnd,
		TotalChanges:   len(records),
		Changes:        make([]ConfigChangeDetail, 0, len(records)),
	}

	for _, record := range records {
		detail := ConfigChangeDetail{
			ChangeID:    record.ID,
			CreatedAt:   record.CreatedAt,
			AppliedAt:   record.AppliedAt,
			Reason:      record.Reason,
			Applicant:   record.Applicant,
			ApplyPolicy: record.ApplyPolicy,
			ChangeType:  record.ChangeType,
			Filename:    record.Filename,
			Component:   getComponentName(record.Filename),
		}

		// Parse modified_parameters JSON
		if record.ModifiedParameters != "" {
			var paramsList []map[string]interface{}
			if err := json.Unmarshal([]byte(record.ModifiedParameters), &paramsList); err == nil {
				for _, param := range paramsList {
					change := parseParameterChange(param)
					change.Component = detail.Component
					detail.ParameterChanges = append(detail.ParameterChanges, change)
				}
			}
		}

		result.Changes = append(result.Changes, detail)
	}

	return result
}

// parseParameterChange parses parameter change from map
func parseParameterChange(param map[string]interface{}) ParameterChange {
	change := ParameterChange{}

	// Parse name
	if v, ok := param["name"].(string); ok {
		change.Name = v
	}

	// Parse from value
	if param["from"] == nil {
		change.From = "kernel default"
	} else if v, ok := param["from"].(string); ok {
		change.From = v
	} else if v, ok := param["from"].(float64); ok {
		change.From = fmt.Sprintf("%.0f", v)
	} else {
		change.From = fmt.Sprintf("%v", param["from"])
	}

	// Parse to value
	if param["to"] == nil {
		change.To = "kernel default"
	} else if v, ok := param["to"].(string); ok {
		change.To = v
	} else if v, ok := param["to"].(float64); ok {
		change.To = fmt.Sprintf("%.0f", v)
	} else {
		change.To = fmt.Sprintf("%v", param["to"])
	}

	// Parse risk level
	if v, ok := param["risk_level"].(string); ok {
		change.RiskLevel = v
	}

	return change
}

// getComponentName maps filename to component name
func getComponentName(filename string) string {
	componentMap := map[string]string{
		"tidb":           "TiDB",
		"tiflash":        "TiFlash",
		"tikv":           "TiKV",
		"pd":             "PD",
		"tiflow":         "TiFlow",
		"tidb-lightning": "TiDB-Lightning",
	}

	if comp, ok := componentMap[filename]; ok {
		return comp
	}
	return filename
}

// uploadToS3 uploads result to S3
func uploadToS3(key string, result *ConfigCheckResult) error {
	region := os.Getenv("AWS_REGION")
	if region == "" {
		region = "us-west-2"
	}
	bucket := os.Getenv("HEALTHCHECK_S3_BUCKET")
	if bucket == "" {
		bucket = "delicated-healthcheck"
	}

	// Role ARN for cross-account access
	assumeRoleARN := os.Getenv("HEALTHCHECK_S3_ROLE_ARN")
	if assumeRoleARN == "" {
		assumeRoleARN = "arn:aws:iam::552185537300:role/clinic-job-role"
	}

	sess, err := session.NewSession(&aws.Config{
		Region: aws.String(region),
	})
	if err != nil {
		return fmt.Errorf("create s3 session failed: %w", err)
	}

	// Create STS client and assume role
	stsClient := sts.New(sess)
	creds := stscreds.NewCredentialsWithClient(stsClient, assumeRoleARN)

	// Create S3 client with assumed role credentials
	s3Client := s3.New(sess, &aws.Config{Credentials: creds})
	uploader := s3manager.NewUploaderWithClient(s3Client)

	// Marshal result to JSON
	jsonData, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal result to json failed: %w", err)
	}

	// Upload to S3
	_, err = uploader.Upload(&s3manager.UploadInput{
		Bucket:      aws.String(bucket),
		Key:         aws.String(key),
		Body:        bytes.NewReader(jsonData),
		ContentType: aws.String("application/json"),
	})
	if err != nil {
		return fmt.Errorf("upload to s3 failed: %w", err)
	}

	return nil
}
