package modules

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type PoolConfig struct {
	Key                *keys.PrivateKey
	Address            string
	DialTimeout        time.Duration
	HealthcheckTimeout time.Duration
	StreamTimeout      time.Duration
	RebalanceInterval  time.Duration
}

func createFrostFS(ctx context.Context, log *zap.Logger, cfg PoolConfig) (authmate.FrostFS, error) {
	log.Debug(logs.PrepareConnectionPool)

	var prm pool.InitParameters
	prm.SetKey(&cfg.Key.PrivateKey)
	prm.SetNodeDialTimeout(cfg.DialTimeout)
	prm.SetHealthcheckTimeout(cfg.HealthcheckTimeout)
	prm.SetNodeStreamTimeout(cfg.StreamTimeout)
	prm.SetClientRebalanceInterval(cfg.RebalanceInterval)
	prm.SetLogger(log)
	prm.AddNode(pool.NewNodeParam(1, cfg.Address, 1))

	p, err := pool.NewPool(prm)
	if err != nil {
		return nil, fmt.Errorf("create pool: %w", err)
	}

	if err = p.Dial(ctx); err != nil {
		return nil, fmt.Errorf("dial pool: %w", err)
	}

	return frostfs.NewAuthmateFrostFS(p, cfg.Key), nil
}

func parsePolicies(val string) (authmate.ContainerPolicies, error) {
	if val == "" {
		return nil, nil
	}

	var (
		data = []byte(val)
		err  error
	)

	if !json.Valid(data) {
		if data, err = os.ReadFile(val); err != nil {
			return nil, fmt.Errorf("coudln't read json file or provided json is invalid")
		}
	}

	var policies authmate.ContainerPolicies
	if err = json.Unmarshal(data, &policies); err != nil {
		return nil, fmt.Errorf("unmarshal policies: %w", err)
	}
	if _, ok := policies[api.DefaultLocationConstraint]; ok {
		return nil, fmt.Errorf("config overrides %s location constraint", api.DefaultLocationConstraint)
	}

	return policies, nil
}

func getJSONRules(val string) ([]byte, error) {
	if val == "" {
		return nil, nil
	}
	data := []byte(val)
	if json.Valid(data) {
		return data, nil
	}

	if data, err := os.ReadFile(val); err == nil {
		if json.Valid(data) {
			return data, nil
		}
	}

	return nil, fmt.Errorf("coudln't read json file or provided json is invalid")
}

// getSessionRules reads json session rules.
// It returns true if rules must be skipped.
func getSessionRules(r string) ([]byte, bool, error) {
	if r == "none" {
		return nil, true, nil
	}

	data, err := getJSONRules(r)
	return data, false, err
}

// getLogger returns new logger depending on appropriate values in viper.Viper
// if logger cannot be built it panics.
func getLogger() *zap.Logger {
	if !viper.GetBool(withLogFlag) {
		return zap.NewNop()
	}

	var zapConfig = zap.Config{
		Development: true,
		Encoding:    "console",
		Level:       zap.NewAtomicLevelAt(zapcore.FatalLevel),
		OutputPaths: []string{"stdout"},
		EncoderConfig: zapcore.EncoderConfig{
			MessageKey:   "message",
			LevelKey:     "level",
			EncodeLevel:  zapcore.CapitalLevelEncoder,
			TimeKey:      "time",
			EncodeTime:   zapcore.ISO8601TimeEncoder,
			CallerKey:    "caller",
			EncodeCaller: zapcore.ShortCallerEncoder,
		},
	}

	if viper.GetBool(debugFlag) {
		zapConfig.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
	}

	log, err := zapConfig.Build()
	if err != nil {
		panic(fmt.Errorf("create logger: %w", err))
	}

	return log
}

func createFrostFSID(ctx context.Context, log *zap.Logger, cfg frostfsid.Config) (authmate.FrostFSID, error) {
	log.Debug(logs.PrepareFrostfsIDClient)

	cli, err := frostfsid.New(ctx, cfg)
	if err != nil {
		return nil, fmt.Errorf("create frostfsid client: %w", err)
	}

	return cli, nil
}

func parseObjectAttrs(attributes string) ([]object.Attribute, error) {
	if len(attributes) == 0 {
		return nil, nil
	}

	rawAttrs := strings.Split(attributes, ",")

	attrs := make([]object.Attribute, len(rawAttrs))
	for i := range rawAttrs {
		k, v, found := strings.Cut(rawAttrs[i], "=")
		if !found {
			return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i])
		}
		attrs[i].SetKey(k)
		attrs[i].SetValue(v)
	}

	return attrs, nil
}