package handlers

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"

	"crowdsec-dashy/internal/crowdsec"
)

// DecisionsHandler manages the decisions page and its POST actions.
type DecisionsHandler struct {
	deps Deps
}

func NewDecisionsHandler(deps Deps) *DecisionsHandler {
	return &DecisionsHandler{deps: deps}
}

// DecisionsData is passed to the decisions template.
type DecisionsData struct {
	PageData
	Decisions []crowdsec.Decision
	Filter    crowdsec.DecisionFilter
	Page      int
	HasNext   bool
}

const decisionsPerPage = 50

var durationRE = regexp.MustCompile(`^\d+[smhdw]$`)

// List renders the paginated decisions list.
func (h *DecisionsHandler) List(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
	defer cancel()

	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
	if page < 1 {
		page = 1
	}

	filter := crowdsec.DecisionFilter{
		Limit:  decisionsPerPage + 1,
		Offset: (page - 1) * decisionsPerPage,
		Type:   r.URL.Query().Get("type"),
		Scope:  r.URL.Query().Get("scope"),
		Value:  r.URL.Query().Get("value"),
		Origin: r.URL.Query().Get("origin"),
	}

	decisions, err := h.deps.LAPI.ListDecisions(ctx, filter)
	if err != nil {
		h.deps.Renderer.RenderError(w, http.StatusBadGateway, "failed to fetch decisions")
		return
	}

	hasNext := len(decisions) > decisionsPerPage
	if hasNext {
		decisions = decisions[:decisionsPerPage]
	}

	pd := NewPageData(r, "Decisions", h.deps.CLIAvailable, h.deps.PollInterval)
	if f := readFlash(r); f.Message != "" {
		pd.Flash = f
	}

	h.deps.Renderer.Render(w, "decisions", DecisionsData{
		PageData:  pd,
		Decisions: decisions,
		Filter:    filter,
		Page:      page,
		HasNext:   hasNext,
	})
}

// Add processes the add-decision form (POST).
func (h *DecisionsHandler) Add(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 4096)
	if err := r.ParseForm(); err != nil {
		flashRedirect(w, r, "/decisions", "error", "invalid form data")
		return
	}
	if !checkCSRF(r) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	value := strings.TrimSpace(r.FormValue("value"))
	scope := r.FormValue("scope")
	decType := r.FormValue("type")
	duration := strings.TrimSpace(r.FormValue("duration"))
	scenario := strings.TrimSpace(r.FormValue("scenario"))

	if err := validateDecisionInput(value, scope, decType, duration); err != nil {
		flashRedirect(w, r, "/decisions", "error", err.Error())
		return
	}

	if scenario == "" {
		scenario = "manual"
	}
	if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_./\-]{1,128}$`, scenario); !matched {
		scenario = "manual"
	}

	ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
	defer cancel()

	if err := h.deps.LAPI.AddDecision(ctx, crowdsec.DecisionInput{
		Duration: duration,
		Origin:   "cscli",
		Scenario: scenario,
		Scope:    scope,
		Type:     decType,
		Value:    value,
	}); err != nil {
		flashRedirect(w, r, "/decisions", "error", fmt.Sprintf("failed to add decision: %v", err))
		return
	}

	flashRedirect(w, r, "/decisions", "success",
		fmt.Sprintf("Decision added: %s %s (%s) for %s", decType, value, scope, duration))
}

// Delete processes decision deletion (POST; `id` field may repeat for bulk).
func (h *DecisionsHandler) Delete(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 4096)
	if err := r.ParseForm(); err != nil {
		flashRedirect(w, r, "/decisions", "error", "invalid form data")
		return
	}
	if !checkCSRF(r) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	ids := r.Form["id"]
	if len(ids) == 0 {
		flashRedirect(w, r, "/decisions", "error", "no decision selected")
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()

	var errs []string
	deleted := 0
	for _, s := range ids {
		id, err := strconv.ParseInt(s, 10, 64)
		if err != nil || id <= 0 {
			errs = append(errs, fmt.Sprintf("invalid id: %q", s))
			continue
		}
		if err := h.deps.LAPI.DeleteDecision(ctx, id); err != nil {
			errs = append(errs, fmt.Sprintf("id %d: %v", id, err))
			continue
		}
		deleted++
	}

	if len(errs) > 0 {
		flashRedirect(w, r, "/decisions", "error", strings.Join(errs, "; "))
		return
	}

	flashRedirect(w, r, "/decisions", "success", fmt.Sprintf("%d decision(s) deleted", deleted))
}

func validateDecisionInput(value, scope, decType, duration string) error {
	switch scope {
	case "Ip", "Range", "Country":
	default:
		return fmt.Errorf("invalid scope: must be Ip, Range, or Country")
	}

	switch decType {
	case "ban", "captcha", "throttle":
	default:
		return fmt.Errorf("invalid type: must be ban, captcha, or throttle")
	}

	if value == "" {
		return fmt.Errorf("value is required")
	}

	switch scope {
	case "Ip":
		if net.ParseIP(value) == nil {
			return fmt.Errorf("invalid IP address: %q", value)
		}
	case "Range":
		if _, _, err := net.ParseCIDR(value); err != nil {
			return fmt.Errorf("invalid CIDR range: %q", value)
		}
	case "Country":
		if len(value) != 2 {
			return fmt.Errorf("invalid country code: must be exactly 2 uppercase letters")
		}
		for _, c := range value {
			if c < 'A' || c > 'Z' {
				return fmt.Errorf("invalid country code: must be uppercase letters")
			}
		}
	}

	if !durationRE.MatchString(duration) {
		return fmt.Errorf("invalid duration %q: use format like 24h, 7d, 30m, 3600s", duration)
	}

	return nil
}
