package crowdsec

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"regexp"
	"strings"
	"time"
)

// CLIClient wraps the cscli binary.
type CLIClient struct {
	cscliPath string
}

// NewCLIClient creates a new CLI client.
// cscliPath should be the absolute path to the cscli binary.
func NewCLIClient(cscliPath string) *CLIClient {
	return &CLIClient{cscliPath: cscliPath}
}

// -----------------------------------------------------------------------
// Bouncers
// -----------------------------------------------------------------------

// ListBouncers returns all registered bouncers.
func (c *CLIClient) ListBouncers(ctx context.Context) ([]Bouncer, error) {
	out, err := c.run(ctx, "bouncers", "list", "-o", "json")
	if err != nil {
		return nil, err
	}

	var bouncers []Bouncer
	if err := json.Unmarshal(out, &bouncers); err != nil {
		return nil, fmt.Errorf("parse bouncers: %w\noutput: %s", err, string(out))
	}
	return bouncers, nil
}

// AddBouncer registers a new bouncer and returns the generated API key.
func (c *CLIClient) AddBouncer(ctx context.Context, name string) (*AddBouncerResult, error) {
	if err := validateName(name); err != nil {
		return nil, err
	}

	// cscli bouncers add <name> -o raw  → prints only the API key
	out, err := c.run(ctx, "bouncers", "add", name, "-o", "raw")
	if err != nil {
		return nil, err
	}

	apiKey := strings.TrimSpace(string(out))
	if apiKey == "" {
		return nil, fmt.Errorf("no API key returned for bouncer %q", name)
	}

	return &AddBouncerResult{Name: name, APIKey: apiKey}, nil
}

// DeleteBouncer removes a bouncer by name.
func (c *CLIClient) DeleteBouncer(ctx context.Context, name string) error {
	if err := validateName(name); err != nil {
		return err
	}
	_, err := c.run(ctx, "bouncers", "delete", name)
	return err
}

// -----------------------------------------------------------------------
// Machines
// -----------------------------------------------------------------------

// ListMachines returns all registered machines.
func (c *CLIClient) ListMachines(ctx context.Context) ([]Machine, error) {
	out, err := c.run(ctx, "machines", "list", "-o", "json")
	if err != nil {
		return nil, err
	}

	var machines []Machine
	if err := json.Unmarshal(out, &machines); err != nil {
		return nil, fmt.Errorf("parse machines: %w\noutput: %s", err, string(out))
	}
	return machines, nil
}

// DeleteMachine removes a machine by ID.
func (c *CLIClient) DeleteMachine(ctx context.Context, machineID string) error {
	if err := validateName(machineID); err != nil {
		return err
	}
	_, err := c.run(ctx, "machines", "delete", "--machine-id", machineID)
	return err
}

// ValidateMachine validates a pending machine registration.
func (c *CLIClient) ValidateMachine(ctx context.Context, machineID string) error {
	if err := validateName(machineID); err != nil {
		return err
	}
	_, err := c.run(ctx, "machines", "validate", machineID)
	return err
}

// -----------------------------------------------------------------------
// Hub — Collections
// -----------------------------------------------------------------------

// ListCollections returns all known collections.
func (c *CLIClient) ListCollections(ctx context.Context) ([]HubItem, error) {
	return c.listHubItems(ctx, "collections")
}

// InstallCollection installs a hub collection.
func (c *CLIClient) InstallCollection(ctx context.Context, name string) error {
	return c.hubAction(ctx, "collections", "install", name)
}

// RemoveCollection removes a hub collection.
func (c *CLIClient) RemoveCollection(ctx context.Context, name string) error {
	return c.hubAction(ctx, "collections", "remove", name)
}

// -----------------------------------------------------------------------
// Hub — Parsers
// -----------------------------------------------------------------------

// ListParsers returns all known parsers.
func (c *CLIClient) ListParsers(ctx context.Context) ([]HubItem, error) {
	return c.listHubItems(ctx, "parsers")
}

// InstallParser installs a parser.
func (c *CLIClient) InstallParser(ctx context.Context, name string) error {
	return c.hubAction(ctx, "parsers", "install", name)
}

// RemoveParser removes a parser.
func (c *CLIClient) RemoveParser(ctx context.Context, name string) error {
	return c.hubAction(ctx, "parsers", "remove", name)
}

// -----------------------------------------------------------------------
// Hub — Scenarios
// -----------------------------------------------------------------------

// ListScenarios returns all known scenarios.
func (c *CLIClient) ListScenarios(ctx context.Context) ([]HubItem, error) {
	return c.listHubItems(ctx, "scenarios")
}

// InstallScenario installs a scenario.
func (c *CLIClient) InstallScenario(ctx context.Context, name string) error {
	return c.hubAction(ctx, "scenarios", "install", name)
}

// RemoveScenario removes a scenario.
func (c *CLIClient) RemoveScenario(ctx context.Context, name string) error {
	return c.hubAction(ctx, "scenarios", "remove", name)
}

// -----------------------------------------------------------------------
// Hub — Postoverflows
// -----------------------------------------------------------------------

// ListPostoverflows returns all known postoverflows.
func (c *CLIClient) ListPostoverflows(ctx context.Context) ([]HubItem, error) {
	return c.listHubItems(ctx, "postoverflows")
}

// HubUpdate runs cscli hub update && cscli hub upgrade.
func (c *CLIClient) HubUpdate(ctx context.Context) error {
	if _, err := c.run(ctx, "hub", "update"); err != nil {
		return fmt.Errorf("hub update: %w", err)
	}
	if _, err := c.run(ctx, "hub", "upgrade"); err != nil {
		return fmt.Errorf("hub upgrade: %w", err)
	}
	return nil
}

// -----------------------------------------------------------------------
// Metrics
// -----------------------------------------------------------------------

// GetMetrics runs cscli metrics and returns parsed sections.
func (c *CLIClient) GetMetrics(ctx context.Context) ([]MetricsSection, error) {
	// cscli metrics does not support JSON output reliably — parse table output
	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	out, err := c.run(ctx, "metrics")
	if err != nil {
		return nil, err
	}

	return parseMetricsOutput(string(out)), nil
}

// GetVersion returns cscli version information.
func (c *CLIClient) GetVersion(ctx context.Context) (string, error) {
	out, err := c.run(ctx, "version")
	if err != nil {
		return "", err
	}
	// version output is plain text like "v1.6.4-..."
	return strings.TrimSpace(string(out)), nil
}

// -----------------------------------------------------------------------
// Internal helpers
// -----------------------------------------------------------------------

// run executes cscli with the given arguments.
// Arguments are passed as a slice — never through a shell — preventing injection.
// Only specific subcommands are allowed.
func (c *CLIClient) run(ctx context.Context, args ...string) ([]byte, error) {
	if err := validateArgs(args); err != nil {
		return nil, err
	}

	ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, c.cscliPath, args...) //nolint:gosec — path validated at startup
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("cscli %s: %w — %s", strings.Join(args, " "), err, stderr.String())
	}

	return stdout.Bytes(), nil
}

// allowedSubcommands is the strict allow-list of cscli first arguments.
var allowedSubcommands = map[string]bool{
	"bouncers":     true,
	"machines":     true,
	"collections":  true,
	"parsers":      true,
	"scenarios":    true,
	"postoverflows": true,
	"hub":          true,
	"metrics":      true,
	"version":      true,
}

// allowedActions for each subcommand.
var allowedActions = map[string]bool{
	"list":      true,
	"add":       true,
	"delete":    true,
	"install":   true,
	"remove":    true,
	"update":    true,
	"upgrade":   true,
	"validate":  true,
}

// safeArg matches strings that are safe to pass as arguments (no shell metacharacters).
var safeArg = regexp.MustCompile(`^[a-zA-Z0-9_./:@\-]+$`)

func validateArgs(args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("no subcommand provided")
	}
	if !allowedSubcommands[args[0]] {
		return fmt.Errorf("disallowed cscli subcommand: %q", args[0])
	}
	// Skip flag-value pairs like "-o json" and "--machine-id <id>" — validate values
	for i := 1; i < len(args); i++ {
		arg := args[i]
		if strings.HasPrefix(arg, "-") {
			continue // flags are fine
		}
		if allowedActions[arg] {
			continue // known action words
		}
		if arg == "raw" || arg == "json" || arg == "human" {
			continue // output format values
		}
		// Everything else (names, IDs) must match safe pattern
		if !safeArg.MatchString(arg) {
			return fmt.Errorf("unsafe cscli argument: %q", arg)
		}
	}
	return nil
}

// validateName checks that a name passed by the user is safe to use as a cscli argument.
var validName = regexp.MustCompile(`^[a-zA-Z0-9_\-]{1,64}$`)

func validateName(name string) error {
	if !validName.MatchString(name) {
		return fmt.Errorf("invalid name %q: must be 1-64 alphanumeric/dash/underscore characters", name)
	}
	return nil
}

func (c *CLIClient) listHubItems(ctx context.Context, kind string) ([]HubItem, error) {
	out, err := c.run(ctx, kind, "list", "-o", "json")
	if err != nil {
		return nil, err
	}

	// cscli returns either {"<kind>": [...]} or just [...]
	// Try array first
	var items []HubItem
	if err := json.Unmarshal(out, &items); err == nil {
		return items, nil
	}

	// Try object wrapper
	var wrapper map[string][]HubItem
	if err := json.Unmarshal(out, &wrapper); err != nil {
		return nil, fmt.Errorf("parse %s: %w\noutput: %s", kind, err, string(out))
	}
	for _, v := range wrapper {
		return v, nil
	}
	return []HubItem{}, nil
}

func (c *CLIClient) hubAction(ctx context.Context, kind, action, name string) error {
	// Hub item names can contain slashes (e.g. crowdsecurity/ssh-bf); use safeArg not validateName.
	if !safeArg.MatchString(name) || name == "" {
		return fmt.Errorf("invalid hub item name: %q", name)
	}
	_, err := c.run(ctx, kind, action, name)
	return err
}

// parseMetricsOutput parses the tabular output of `cscli metrics`.
// It looks for lines that look like table headers (─── separators).
func parseMetricsOutput(output string) []MetricsSection {
	var sections []MetricsSection
	var current *MetricsSection
	var headerFound bool

	scanner := bufio.NewScanner(strings.NewReader(output))
	for scanner.Scan() {
		line := scanner.Text()

		// Section title lines — plain text before the table
		if !strings.Contains(line, "│") && !strings.Contains(line, "─") &&
			strings.TrimSpace(line) != "" && !strings.HasPrefix(strings.TrimSpace(line), "time=") {
			if current != nil && len(current.Rows) > 0 {
				sections = append(sections, *current)
			}
			current = &MetricsSection{Title: strings.TrimSpace(line)}
			headerFound = false
			continue
		}

		if current == nil {
			continue
		}

		// Header row (contains │ and no numbers)
		if strings.Contains(line, "│") && !headerFound {
			parts := splitTableRow(line)
			if len(parts) > 0 {
				current.Headers = parts
				headerFound = true
			}
			continue
		}

		// Separator lines
		if strings.Contains(line, "─") {
			continue
		}

		// Data rows
		if strings.Contains(line, "│") && headerFound {
			parts := splitTableRow(line)
			if len(parts) > 0 {
				current.Rows = append(current.Rows, parts)
			}
		}
	}

	if current != nil && len(current.Rows) > 0 {
		sections = append(sections, *current)
	}

	return sections
}

func splitTableRow(line string) []string {
	parts := strings.Split(line, "│")
	var cells []string
	for _, p := range parts {
		trimmed := strings.TrimSpace(p)
		if trimmed != "" {
			cells = append(cells, trimmed)
		}
	}
	return cells
}
