package http

import (
	"encoding/json"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/rs/xid"

	"github.com/netbirdio/netbird/management/server"
	"github.com/netbirdio/netbird/management/server/http/api"
	"github.com/netbirdio/netbird/management/server/http/util"
	"github.com/netbirdio/netbird/management/server/jwtclaims"
	"github.com/netbirdio/netbird/management/server/status"
)

// RulesHandler is a handler that returns rules of the account
type RulesHandler struct {
	accountManager  server.AccountManager
	claimsExtractor *jwtclaims.ClaimsExtractor
}

// NewRulesHandler creates a new RulesHandler HTTP handler
func NewRulesHandler(accountManager server.AccountManager, authCfg AuthCfg) *RulesHandler {
	return &RulesHandler{
		accountManager: accountManager,
		claimsExtractor: jwtclaims.NewClaimsExtractor(
			jwtclaims.WithAudience(authCfg.Audience),
			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
		),
	}
}

// GetAllRules list for the account
func (h *RulesHandler) GetAllRules(w http.ResponseWriter, r *http.Request) {
	claims := h.claimsExtractor.FromRequestContext(r)
	account, user, err := h.accountManager.GetAccountFromToken(claims)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	accountPolicies, err := h.accountManager.ListPolicies(account.Id, user.Id)
	if err != nil {
		util.WriteError(err, w)
		return
	}
	rules := []*api.Rule{}
	for _, policy := range accountPolicies {
		for _, r := range policy.Rules {
			rules = append(rules, toRuleResponse(account, r.ToRule()))
		}
	}

	util.WriteJSONObject(w, rules)
}

// UpdateRule handles update to a rule identified by a given ID
func (h *RulesHandler) UpdateRule(w http.ResponseWriter, r *http.Request) {
	claims := h.claimsExtractor.FromRequestContext(r)
	account, user, err := h.accountManager.GetAccountFromToken(claims)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	vars := mux.Vars(r)
	ruleID := vars["ruleId"]
	if len(ruleID) == 0 {
		util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
		return
	}

	policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	var req api.PutApiRulesRuleIdJSONRequestBody
	err = json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
	}

	if req.Name == "" {
		util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
		return
	}

	var reqSources []string
	if req.Sources != nil {
		reqSources = *req.Sources
	}

	var reqDestinations []string
	if req.Destinations != nil {
		reqDestinations = *req.Destinations
	}

	if len(policy.Rules) != 1 {
		util.WriteError(status.Errorf(status.Internal, "policy should contain exactly one rule"), w)
		return
	}

	policy.Name = req.Name
	policy.Description = req.Description
	policy.Enabled = !req.Disabled
	policy.Rules[0].ID = ruleID
	policy.Rules[0].Name = req.Name
	policy.Rules[0].Sources = reqSources
	policy.Rules[0].Destinations = reqDestinations
	policy.Rules[0].Enabled = !req.Disabled
	policy.Rules[0].Description = req.Description

	switch req.Flow {
	case server.TrafficFlowBidirectString:
		policy.Rules[0].Action = server.PolicyTrafficActionAccept
	default:
		util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w)
		return
	}

	err = h.accountManager.SavePolicy(account.Id, user.Id, policy)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	resp := toRuleResponse(account, policy.Rules[0].ToRule())

	util.WriteJSONObject(w, &resp)
}

// CreateRule handles rule creation request
func (h *RulesHandler) CreateRule(w http.ResponseWriter, r *http.Request) {
	claims := h.claimsExtractor.FromRequestContext(r)
	account, user, err := h.accountManager.GetAccountFromToken(claims)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	var req api.PostApiRulesJSONRequestBody
	err = json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
		return
	}

	if req.Name == "" {
		util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
		return
	}

	var reqSources []string
	if req.Sources != nil {
		reqSources = *req.Sources
	}

	var reqDestinations []string
	if req.Destinations != nil {
		reqDestinations = *req.Destinations
	}

	rule := server.Rule{
		ID:          xid.New().String(),
		Name:        req.Name,
		Source:      reqSources,
		Destination: reqDestinations,
		Disabled:    req.Disabled,
		Description: req.Description,
	}

	switch req.Flow {
	case server.TrafficFlowBidirectString:
		rule.Flow = server.TrafficFlowBidirect
	default:
		util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w)
		return
	}

	policy, err := server.RuleToPolicy(&rule)
	if err != nil {
		util.WriteError(err, w)
		return
	}
	err = h.accountManager.SavePolicy(account.Id, user.Id, policy)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	resp := toRuleResponse(account, &rule)

	util.WriteJSONObject(w, &resp)
}

// DeleteRule handles rule deletion request
func (h *RulesHandler) DeleteRule(w http.ResponseWriter, r *http.Request) {
	claims := h.claimsExtractor.FromRequestContext(r)
	account, user, err := h.accountManager.GetAccountFromToken(claims)
	if err != nil {
		util.WriteError(err, w)
		return
	}
	aID := account.Id

	rID := mux.Vars(r)["ruleId"]
	if len(rID) == 0 {
		util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
		return
	}

	err = h.accountManager.DeletePolicy(aID, rID, user.Id)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	util.WriteJSONObject(w, emptyObject{})
}

// GetRule handles a group Get request identified by ID
func (h *RulesHandler) GetRule(w http.ResponseWriter, r *http.Request) {
	claims := h.claimsExtractor.FromRequestContext(r)
	account, user, err := h.accountManager.GetAccountFromToken(claims)
	if err != nil {
		util.WriteError(err, w)
		return
	}

	switch r.Method {
	case http.MethodGet:
		ruleID := mux.Vars(r)["ruleId"]
		if len(ruleID) == 0 {
			util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
			return
		}

		policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id)
		if err != nil {
			util.WriteError(err, w)
			return
		}

		util.WriteJSONObject(w, toRuleResponse(account, policy.Rules[0].ToRule()))
	default:
		util.WriteError(status.Errorf(status.NotFound, "method not found"), w)
	}
}

func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule {
	cache := make(map[string]api.GroupMinimum)
	gr := api.Rule{
		Id:          rule.ID,
		Name:        rule.Name,
		Description: rule.Description,
		Disabled:    rule.Disabled,
	}

	switch rule.Flow {
	case server.TrafficFlowBidirect:
		gr.Flow = server.TrafficFlowBidirectString
	default:
		gr.Flow = "unknown"
	}

	for _, gid := range rule.Source {
		_, ok := cache[gid]
		if ok {
			continue
		}

		if group, ok := account.Groups[gid]; ok {
			minimum := api.GroupMinimum{
				Id:         group.ID,
				Name:       group.Name,
				PeersCount: len(group.Peers),
			}

			gr.Sources = append(gr.Sources, minimum)
			cache[gid] = minimum
		}
	}

	for _, gid := range rule.Destination {
		cachedMinimum, ok := cache[gid]
		if ok {
			gr.Destinations = append(gr.Destinations, cachedMinimum)
			continue
		}
		if group, ok := account.Groups[gid]; ok {
			minimum := api.GroupMinimum{
				Id:         group.ID,
				Name:       group.Name,
				PeersCount: len(group.Peers),
			}
			gr.Destinations = append(gr.Destinations, minimum)
			cache[gid] = minimum
		}
	}

	return &gr
}
