// Package crowdsec provides integration with CrowdSec for security decisions and remediation.
package crowdsec

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"
)

const (
	// defaultLAPIURL is the default CrowdSec LAPI URL.
	// Port 8085 is used to avoid conflict with Charon management API on port 8080.
	defaultLAPIURL          = "http://127.0.0.1:8085"
	defaultHealthTimeout    = 5 * time.Second
	defaultRegistrationName = "caddy-bouncer"
)

// BouncerRegistration holds information about a registered bouncer.
type BouncerRegistration struct {
	Name      string    `json:"name"`
	APIKey    string    `json:"api_key"`
	IPAddress string    `json:"ip_address,omitempty"`
	Valid     bool      `json:"valid"`
	CreatedAt time.Time `json:"created_at,omitempty"`
}

// LAPIHealthResponse represents the health check response from CrowdSec LAPI.
type LAPIHealthResponse struct {
	Message string `json:"message,omitempty"`
	Version string `json:"version,omitempty"`
}

// EnsureBouncerRegistered checks if a caddy bouncer is registered with CrowdSec LAPI.
// If not registered and cscli is available, it will attempt to register one.
// Returns the API key for the bouncer (from env var or newly registered).
func EnsureBouncerRegistered(ctx context.Context, lapiURL string) (string, error) {
	// First check if API key is provided via environment
	apiKey := getBouncerAPIKey()
	if apiKey != "" {
		return apiKey, nil
	}

	// Check if cscli is available
	if !hasCSCLI() {
		return "", fmt.Errorf("no API key provided and cscli not available for bouncer registration")
	}

	// Check if bouncer already exists
	existing, err := getExistingBouncer(ctx, defaultRegistrationName)
	if err == nil && existing.APIKey != "" {
		return existing.APIKey, nil
	}

	// Register new bouncer using cscli
	return registerBouncer(ctx, defaultRegistrationName)
}

// CheckLAPIHealth verifies CrowdSec LAPI is responding.
func CheckLAPIHealth(lapiURL string) bool {
	if lapiURL == "" {
		lapiURL = defaultLAPIURL
	}

	ctx, cancel := context.WithTimeout(context.Background(), defaultHealthTimeout)
	defer cancel()

	// Try the /health endpoint first (standard LAPI health check)
	healthURL := strings.TrimRight(lapiURL, "/") + "/health"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, http.NoBody)
	if err != nil {
		return false
	}

	client := &http.Client{Timeout: defaultHealthTimeout}
	resp, err := client.Do(req)
	if err != nil {
		// Fallback: try the /v1/decisions endpoint with a HEAD request
		return checkDecisionsEndpoint(ctx, lapiURL)
	}
	defer resp.Body.Close()

	// Check content-type to ensure we're getting JSON from actual LAPI (not HTML from frontend)
	contentType := resp.Header.Get("Content-Type")
	if contentType != "" && !strings.Contains(contentType, "application/json") {
		// Not JSON response, likely hitting a frontend/proxy
		return false
	}

	// LAPI returns 200 OK for healthy status
	if resp.StatusCode == http.StatusOK {
		return true
	}

	// If health endpoint returned non-OK, try decisions endpoint fallback
	if resp.StatusCode == http.StatusNotFound {
		return checkDecisionsEndpoint(ctx, lapiURL)
	}

	return false
}

// GetLAPIVersion retrieves the CrowdSec LAPI version.
func GetLAPIVersion(ctx context.Context, lapiURL string) (string, error) {
	if lapiURL == "" {
		lapiURL = defaultLAPIURL
	}

	versionURL := strings.TrimRight(lapiURL, "/") + "/v1/version"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionURL, http.NoBody)
	if err != nil {
		return "", fmt.Errorf("create version request: %w", err)
	}

	client := &http.Client{Timeout: defaultHealthTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("version request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("version request returned status %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("read version response: %w", err)
	}

	var versionResp struct {
		Version string `json:"version"`
	}
	if err := json.Unmarshal(body, &versionResp); err != nil {
		// Some versions return plain text
		return strings.TrimSpace(string(body)), nil
	}

	return versionResp.Version, nil
}

// checkDecisionsEndpoint is a fallback health check using the decisions endpoint.
func checkDecisionsEndpoint(ctx context.Context, lapiURL string) bool {
	decisionsURL := strings.TrimRight(lapiURL, "/") + "/v1/decisions"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, decisionsURL, http.NoBody)
	if err != nil {
		return false
	}

	client := &http.Client{Timeout: defaultHealthTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	defer resp.Body.Close()

	// Check content-type to avoid false positives from HTML responses
	contentType := resp.Header.Get("Content-Type")
	if contentType != "" && !strings.Contains(contentType, "application/json") {
		// Not JSON response, likely hitting a frontend/proxy
		return false
	}

	// 401 is expected without auth, but indicates LAPI is running
	// 200 with empty array is also valid (no decisions)
	return resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized
}

// getBouncerAPIKey returns the bouncer API key from environment variables.
func getBouncerAPIKey() string {
	// Check multiple possible env var names for the API key
	envVars := []string{
		"CROWDSEC_API_KEY",
		"CROWDSEC_BOUNCER_API_KEY",
		"CERBERUS_SECURITY_CROWDSEC_API_KEY",
		"CHARON_SECURITY_CROWDSEC_API_KEY",
		"CPM_SECURITY_CROWDSEC_API_KEY",
	}

	for _, key := range envVars {
		if val := os.Getenv(key); val != "" {
			return val
		}
	}
	return ""
}

// hasCSCLI checks if cscli command is available.
func hasCSCLI() bool {
	_, err := exec.LookPath("cscli")
	return err == nil
}

// getExistingBouncer retrieves an existing bouncer registration by name.
func getExistingBouncer(ctx context.Context, name string) (BouncerRegistration, error) {
	cmd := exec.CommandContext(ctx, "cscli", "bouncers", "list", "-o", "json")
	output, err := cmd.Output()
	if err != nil {
		return BouncerRegistration{}, fmt.Errorf("list bouncers: %w", err)
	}

	var bouncers []struct {
		Name      string `json:"name"`
		APIKey    string `json:"api_key"`
		IPAddress string `json:"ip_address"`
		Valid     bool   `json:"valid"`
		CreatedAt string `json:"created_at"`
	}

	if err := json.Unmarshal(output, &bouncers); err != nil {
		return BouncerRegistration{}, fmt.Errorf("parse bouncers: %w", err)
	}

	for _, b := range bouncers {
		if b.Name == name {
			var createdAt time.Time
			if b.CreatedAt != "" {
				createdAt, _ = time.Parse(time.RFC3339, b.CreatedAt)
			}
			return BouncerRegistration{
				Name:      b.Name,
				APIKey:    b.APIKey,
				IPAddress: b.IPAddress,
				Valid:     b.Valid,
				CreatedAt: createdAt,
			}, nil
		}
	}

	return BouncerRegistration{}, fmt.Errorf("bouncer %q not found", name)
}

// registerBouncer registers a new bouncer with CrowdSec using cscli.
func registerBouncer(ctx context.Context, name string) (string, error) {
	cmd := exec.CommandContext(ctx, "cscli", "bouncers", "add", name, "-o", "raw")
	output, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("register bouncer: %w", err)
	}

	apiKey := strings.TrimSpace(string(output))
	if apiKey == "" {
		return "", fmt.Errorf("empty API key returned from bouncer registration")
	}

	return apiKey, nil
}
