package crowdsec

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"gorm.io/gorm"

	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/Wikid82/charon/backend/internal/models"
)

const (
	consoleStatusNotEnrolled = "not_enrolled"
	consoleStatusEnrolling   = "enrolling"
	consoleStatusEnrolled    = "enrolled"
	consoleStatusFailed      = "failed"

	defaultEnrollTimeout = 45 * time.Second
)

var namePattern = regexp.MustCompile(`^[A-Za-z0-9_.\-]{1,64}$`)
var enrollmentTokenPattern = regexp.MustCompile(`^[A-Za-z0-9]{10,64}$`)

// EnvCommandExecutor executes commands with optional environment overrides.
type EnvCommandExecutor interface {
	ExecuteWithEnv(ctx context.Context, name string, args []string, env map[string]string) ([]byte, error)
}

// SecureCommandExecutor is the production executor that avoids leaking args by passing secrets via env.
type SecureCommandExecutor struct{}

// ExecuteWithEnv runs the command with provided env merged onto the current environment.
func (r *SecureCommandExecutor) ExecuteWithEnv(ctx context.Context, name string, args []string, env map[string]string) ([]byte, error) {
	cmd := exec.CommandContext(ctx, name, args...)
	cmd.Env = append(os.Environ(), formatEnv(env)...)
	return cmd.CombinedOutput()
}

func formatEnv(env map[string]string) []string {
	if len(env) == 0 {
		return nil
	}
	result := make([]string, 0, len(env))
	for k, v := range env {
		result = append(result, fmt.Sprintf("%s=%s", k, v))
	}
	return result
}

// ConsoleEnrollRequest captures enrollment input.
type ConsoleEnrollRequest struct {
	EnrollmentKey string
	Tenant        string
	AgentName     string
	Force         bool
}

// ConsoleEnrollmentStatus is the safe, redacted status view.
type ConsoleEnrollmentStatus struct {
	Status          string     `json:"status"`
	Tenant          string     `json:"tenant"`
	AgentName       string     `json:"agent_name"`
	LastError       string     `json:"last_error,omitempty"`
	LastAttemptAt   *time.Time `json:"last_attempt_at,omitempty"`
	EnrolledAt      *time.Time `json:"enrolled_at,omitempty"`
	LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"`
	KeyPresent      bool       `json:"key_present"`
	CorrelationID   string     `json:"correlation_id,omitempty"`
}

// ConsoleEnrollmentService manages console enrollment lifecycle and persistence.
type ConsoleEnrollmentService struct {
	db      *gorm.DB
	exec    EnvCommandExecutor
	dataDir string
	key     []byte
	nowFn   func() time.Time
	mu      sync.Mutex
	timeout time.Duration
}

// NewConsoleEnrollmentService constructs a service using the supplied secret material for encryption.
func NewConsoleEnrollmentService(db *gorm.DB, executor EnvCommandExecutor, dataDir, secret string) *ConsoleEnrollmentService {
	return &ConsoleEnrollmentService{
		db:      db,
		exec:    executor,
		dataDir: dataDir,
		key:     deriveKey(secret),
		nowFn:   time.Now,
		timeout: defaultEnrollTimeout,
	}
}

// Status returns the current enrollment state.
func (s *ConsoleEnrollmentService) Status(ctx context.Context) (ConsoleEnrollmentStatus, error) {
	rec, err := s.load(ctx)
	if err != nil {
		return ConsoleEnrollmentStatus{}, err
	}
	return s.statusFromModel(rec), nil
}

// Enroll performs an enrollment attempt. It is idempotent when already enrolled unless Force is set.
func (s *ConsoleEnrollmentService) Enroll(ctx context.Context, req ConsoleEnrollRequest) (ConsoleEnrollmentStatus, error) {
	agent := strings.TrimSpace(req.AgentName)
	if agent == "" {
		return ConsoleEnrollmentStatus{}, fmt.Errorf("agent_name required")
	}
	if !namePattern.MatchString(agent) {
		return ConsoleEnrollmentStatus{}, fmt.Errorf("agent_name may only include letters, numbers, dot, dash, underscore")
	}
	tenant := strings.TrimSpace(req.Tenant)
	if tenant != "" && !namePattern.MatchString(tenant) {
		return ConsoleEnrollmentStatus{}, fmt.Errorf("tenant may only include letters, numbers, dot, dash, underscore")
	}
	token, err := normalizeEnrollmentKey(req.EnrollmentKey)
	if err != nil {
		return ConsoleEnrollmentStatus{}, err
	}
	if s.exec == nil {
		return ConsoleEnrollmentStatus{}, fmt.Errorf("executor unavailable")
	}

	if err := s.ensureCAPIRegistered(ctx); err != nil {
		return ConsoleEnrollmentStatus{}, err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	rec, err := s.load(ctx)
	if err != nil {
		return ConsoleEnrollmentStatus{}, err
	}

	if rec.Status == consoleStatusEnrolling {
		return s.statusFromModel(rec), fmt.Errorf("enrollment already in progress")
	}
	if rec.Status == consoleStatusEnrolled && !req.Force {
		return s.statusFromModel(rec), nil
	}

	now := s.nowFn().UTC()
	rec.Status = consoleStatusEnrolling
	rec.AgentName = agent
	rec.Tenant = tenant
	rec.LastAttemptAt = &now
	rec.LastError = ""
	rec.LastCorrelationID = uuid.NewString()

	encryptedKey, err := s.encrypt(token)
	if err != nil {
		return ConsoleEnrollmentStatus{}, fmt.Errorf("protect secret: %w", err)
	}
	rec.EncryptedEnrollKey = encryptedKey

	if err := s.db.WithContext(ctx).Save(rec).Error; err != nil {
		return ConsoleEnrollmentStatus{}, err
	}

	cmdCtx, cancel := context.WithTimeout(ctx, s.timeout)
	defer cancel()

	args := []string{"console", "enroll", "--name", agent}
	if _, err := os.Stat(filepath.Join(s.dataDir, "config.yaml")); err == nil {
		args = append([]string{"-c", filepath.Join(s.dataDir, "config.yaml")}, args...)
	}
	args = append(args, token)

	logger.Log().WithField("tenant", tenant).WithField("agent", agent).WithField("correlation_id", rec.LastCorrelationID).Info("starting crowdsec console enrollment")
	out, cmdErr := s.exec.ExecuteWithEnv(cmdCtx, "cscli", args, nil)

	if cmdErr != nil {
		rec.Status = consoleStatusFailed
		rec.LastError = redactSecret(string(out)+": "+cmdErr.Error(), token)
		_ = s.db.WithContext(ctx).Save(rec)
		logger.Log().WithError(cmdErr).WithField("correlation_id", rec.LastCorrelationID).WithField("tenant", tenant).Warn("crowdsec console enrollment failed")
		return s.statusFromModel(rec), fmt.Errorf("console enrollment failed: %s", rec.LastError)
	}

	complete := s.nowFn().UTC()
	rec.Status = consoleStatusEnrolled
	rec.EnrolledAt = &complete
	rec.LastHeartbeatAt = &complete
	rec.LastError = ""
	if err := s.db.WithContext(ctx).Save(rec).Error; err != nil {
		return ConsoleEnrollmentStatus{}, err
	}

	logger.Log().WithField("tenant", tenant).WithField("agent", agent).WithField("correlation_id", rec.LastCorrelationID).Info("crowdsec console enrollment succeeded")
	return s.statusFromModel(rec), nil
}

func (s *ConsoleEnrollmentService) ensureCAPIRegistered(ctx context.Context) error {
	credsPath := filepath.Join(s.dataDir, "online_api_credentials.yaml")
	if _, err := os.Stat(credsPath); err == nil {
		return nil
	}

	logger.Log().Info("registering with crowdsec capi")
	args := []string{"capi", "register"}
	if _, err := os.Stat(filepath.Join(s.dataDir, "config.yaml")); err == nil {
		args = append([]string{"-c", filepath.Join(s.dataDir, "config.yaml")}, args...)
	}

	if _, err := s.exec.ExecuteWithEnv(ctx, "cscli", args, nil); err != nil {
		return fmt.Errorf("capi register: %w", err)
	}
	return nil
}

func (s *ConsoleEnrollmentService) load(ctx context.Context) (*models.CrowdsecConsoleEnrollment, error) {
	var rec models.CrowdsecConsoleEnrollment
	err := s.db.WithContext(ctx).First(&rec).Error
	if err == nil {
		return &rec, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	now := s.nowFn().UTC()
	rec = models.CrowdsecConsoleEnrollment{
		UUID:      uuid.NewString(),
		Status:    consoleStatusNotEnrolled,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := s.db.WithContext(ctx).Create(&rec).Error; err != nil {
		return nil, err
	}
	return &rec, nil
}

func (s *ConsoleEnrollmentService) statusFromModel(rec *models.CrowdsecConsoleEnrollment) ConsoleEnrollmentStatus {
	if rec == nil {
		return ConsoleEnrollmentStatus{Status: consoleStatusNotEnrolled}
	}
	return ConsoleEnrollmentStatus{
		Status:          firstNonEmpty(rec.Status, consoleStatusNotEnrolled),
		Tenant:          rec.Tenant,
		AgentName:       rec.AgentName,
		LastError:       rec.LastError,
		LastAttemptAt:   rec.LastAttemptAt,
		EnrolledAt:      rec.EnrolledAt,
		LastHeartbeatAt: rec.LastHeartbeatAt,
		KeyPresent:      rec.EncryptedEnrollKey != "",
		CorrelationID:   rec.LastCorrelationID,
	}
}

func (s *ConsoleEnrollmentService) encrypt(value string) (string, error) {
	if value == "" {
		return "", nil
	}
	block, err := aes.NewCipher(s.key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return "", err
	}
	sealed := gcm.Seal(nonce, nonce, []byte(value), nil)
	return base64.StdEncoding.EncodeToString(sealed), nil
}

// decrypt is only used in tests to validate encryption roundtrips.
func (s *ConsoleEnrollmentService) decrypt(value string) (string, error) {
	if value == "" {
		return "", nil
	}
	ciphertext, err := base64.StdEncoding.DecodeString(value)
	if err != nil {
		return "", err
	}
	block, err := aes.NewCipher(s.key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return "", fmt.Errorf("ciphertext too short")
	}
	nonce, ct := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plain, err := gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return "", err
	}
	return string(plain), nil
}

func deriveKey(secret string) []byte {
	if secret == "" {
		secret = "charon-console-enroll-default"
	}
	sum := sha256.Sum256([]byte(secret))
	return sum[:]
}

func redactSecret(msg, secret string) string {
	if secret == "" {
		return msg
	}
	return strings.ReplaceAll(msg, secret, "<redacted>")
}

func normalizeEnrollmentKey(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", fmt.Errorf("enrollment_key required")
	}
	if enrollmentTokenPattern.MatchString(trimmed) {
		return trimmed, nil
	}

	parts := strings.Fields(trimmed)
	if len(parts) == 0 {
		return "", fmt.Errorf("invalid enrollment key")
	}
	if strings.EqualFold(parts[0], "sudo") {
		parts = parts[1:]
	}

	if len(parts) == 4 && parts[0] == "cscli" && parts[1] == "console" && parts[2] == "enroll" {
		token := parts[3]
		if enrollmentTokenPattern.MatchString(token) {
			return token, nil
		}
	}

	return "", fmt.Errorf("invalid enrollment key")
}
