package ape

import (
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"

	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	apeutil "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/ape"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"github.com/nspcc-dev/neo-go/cli/input"
	"github.com/spf13/cobra"
)

const (
	defaultNamespace = "root"
	namespaceTarget  = "namespace"
	containerTarget  = "container"
	userTarget       = "user"
	groupTarget      = "group"

	Ingress = "ingress"
	S3      = "s3"
)

var mChainName = map[string]apechain.Name{
	Ingress: apechain.Ingress,
	S3:      apechain.S3,
}

var (
	errSettingDefaultValueWasDeclined = errors.New("setting default value was declined")
	errUnknownTargetType              = errors.New("unknown target type")
	errUnsupportedChainName           = errors.New("unsupported chain name")
)

// PrintHumanReadableAPEChain print APE chain rules.
func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) {
	cmd.Println("Chain ID: " + string(chain.ID))
	cmd.Printf("     HEX: %x\n", chain.ID)
	cmd.Println("Rules:")
	for _, rule := range chain.Rules {
		cmd.Println("\n\tStatus: " + rule.Status.String())
		cmd.Println("\tAny: " + strconv.FormatBool(rule.Any))
		cmd.Println("\tConditions:")
		for _, c := range rule.Condition {
			var ot string
			switch c.Kind {
			case apechain.KindResource:
				ot = "Resource"
			case apechain.KindRequest:
				ot = "Request"
			default:
				panic("unknown object type")
			}
			cmd.Println(fmt.Sprintf("\t\t%s %s %s %s", ot, c.Key, c.Op, c.Value))
		}
		cmd.Println("\tActions:\tInverted:" + strconv.FormatBool(rule.Actions.Inverted))
		for _, name := range rule.Actions.Names {
			cmd.Println("\t\t" + name)
		}
		cmd.Println("\tResources:\tInverted:" + strconv.FormatBool(rule.Resources.Inverted))
		for _, name := range rule.Resources.Names {
			cmd.Println("\t\t" + name)
		}
	}
}

// ParseTarget handles target parsing of an APE chain.
func ParseTarget(cmd *cobra.Command) engine.Target {
	typ := ParseTargetType(cmd)
	name, _ := cmd.Flags().GetString(TargetNameFlag)
	switch typ {
	case engine.Namespace:
		if name == "" {
			ln, err := input.ReadLine(fmt.Sprintf("Target name is not set. Confirm to use %s namespace (n|Y)> ", defaultNamespace))
			commonCmd.ExitOnErr(cmd, "read line error: %w", err)
			ln = strings.ToLower(ln)
			if len(ln) > 0 && (ln[0] == 'n') {
				commonCmd.ExitOnErr(cmd, "read namespace error: %w", errSettingDefaultValueWasDeclined)
			}
			name = defaultNamespace
		}
		return engine.NamespaceTarget(name)
	case engine.Container:
		var cnr cid.ID
		commonCmd.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(name))
		return engine.ContainerTarget(name)
	case engine.User:
		return engine.UserTarget(name)
	case engine.Group:
		return engine.GroupTarget(name)
	default:
		commonCmd.ExitOnErr(cmd, "read target type error: %w", errUnknownTargetType)
	}
	panic("unreachable")
}

// ParseTargetType handles target type parsing of an APE chain.
func ParseTargetType(cmd *cobra.Command) engine.TargetType {
	typ, _ := cmd.Flags().GetString(TargetTypeFlag)
	switch typ {
	case namespaceTarget:
		return engine.Namespace
	case containerTarget:
		return engine.Container
	case userTarget:
		return engine.User
	case groupTarget:
		return engine.Group
	default:
		commonCmd.ExitOnErr(cmd, "parse target type error: %w", errUnknownTargetType)
	}
	panic("unreachable")
}

// ParseChainID handles the parsing of APE-chain identifier.
// For some subcommands, chain ID is optional as an input parameter and should be generated by
// the service instead.
func ParseChainID(cmd *cobra.Command) (id apechain.ID) {
	chainID, _ := cmd.Flags().GetString(ChainIDFlag)
	id = apechain.ID(chainID)

	hexEncoded, _ := cmd.Flags().GetBool(ChainIDHexFlag)
	if !hexEncoded {
		return
	}

	chainIDRaw, err := hex.DecodeString(chainID)
	commonCmd.ExitOnErr(cmd, "can't decode chain ID as hex: %w", err)
	id = apechain.ID(chainIDRaw)
	return
}

// ParseChain parses an APE chain which can be provided either as a rule statement
// or loaded from a binary/JSON file path.
func ParseChain(cmd *cobra.Command) *apechain.Chain {
	chain := new(apechain.Chain)
	chain.ID = ParseChainID(cmd)

	if rules, _ := cmd.Flags().GetStringArray(RuleFlag); len(rules) > 0 {
		commonCmd.ExitOnErr(cmd, "parser error: %w", apeutil.ParseAPEChain(chain, rules))
	} else if encPath, _ := cmd.Flags().GetString(PathFlag); encPath != "" {
		commonCmd.ExitOnErr(cmd, "decode binary or json error: %w", apeutil.ParseAPEChainBinaryOrJSON(chain, encPath))
	} else {
		commonCmd.ExitOnErr(cmd, "parser error", errors.New("rule is not passed"))
	}

	cmd.Println("Parsed chain:")
	PrintHumanReadableAPEChain(cmd, chain)

	return chain
}

// ParseChainName parses chain name: the place in the request lifecycle where policy is applied.
func ParseChainName(cmd *cobra.Command) apechain.Name {
	chainName, _ := cmd.Flags().GetString(ChainNameFlag)
	apeChainName, ok := mChainName[strings.ToLower(chainName)]
	if !ok {
		commonCmd.ExitOnErr(cmd, "", errUnsupportedChainName)
	}
	return apeChainName
}