package kv

import (
	"context"
	"os"
	"strings"

	"github.com/hashicorp/vault/api"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

const defaultComponent = "vault kv"

type service struct {
	logger    mlogger.Logger
	component string
	store     *api.KVv2
}

func newService(opts Options) (Client, error) {
	logger := opts.Logger
	if logger == nil {
		logger = zap.NewNop()
	}

	component := strings.TrimSpace(opts.Component)
	if component == "" {
		component = defaultComponent
	}

	address := strings.TrimSpace(opts.Config.Address)
	if address == "" {
		logger.Error("Vault address missing")
		return nil, merrors.InvalidArgument(component + ": address is required")
	}

	token, tokenSource, err := resolveToken(opts.Config)
	if err != nil {
		logger.Error("Vault token configuration is invalid", zap.Error(err))
		return nil, err
	}
	if token == "" {
		logger.Error("Vault token missing", zap.String("source", tokenSource))
		return nil, merrors.InvalidArgument(component + ": vault token is empty")
	}

	mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/")
	if mountPath == "" {
		logger.Error("Vault mount path missing")
		return nil, merrors.InvalidArgument(component + ": mount_path is required")
	}

	clientCfg := api.DefaultConfig()
	clientCfg.Address = address

	client, err := api.NewClient(clientCfg)
	if err != nil {
		logger.Error("Failed to create vault client", zap.Error(err))
		return nil, merrors.Internal(component + ": failed to create client: " + err.Error())
	}
	client.SetToken(token)

	if ns := strings.TrimSpace(opts.Config.Namespace); ns != "" {
		client.SetNamespace(ns)
	}

	return &service{
		logger:    logger.Named("vault"),
		component: component,
		store:     client.KVv2(mountPath),
	}, nil
}

func (s *service) Put(ctx context.Context, secretPath string, payload map[string]interface{}) error {
	if payload == nil {
		return merrors.InvalidArgument(s.component+": payload is required", "payload")
	}

	normalizedPath, err := normalizePath(secretPath)
	if err != nil {
		return merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path")
	}

	if _, err := s.store.Put(ctx, normalizedPath, payload); err != nil {
		s.logger.Warn("Failed to write secret", zap.String("path", normalizedPath), zap.Error(err))
		return merrors.Internal(s.component + ": failed to write secret at " + normalizedPath + ": " + err.Error())
	}

	return nil
}

func (s *service) Get(ctx context.Context, secretPath string) (map[string]interface{}, error) {
	normalizedPath, err := normalizePath(secretPath)
	if err != nil {
		return nil, merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path")
	}

	secret, err := s.store.Get(ctx, normalizedPath)
	if err != nil {
		s.logger.Warn("Failed to read secret", zap.String("path", normalizedPath), zap.Error(err))
		return nil, merrors.Internal(s.component + ": failed to read secret at " + normalizedPath + ": " + err.Error())
	}
	if secret == nil || secret.Data == nil {
		return nil, merrors.NoData(s.component + ": secret " + normalizedPath + " not found")
	}

	data := make(map[string]interface{}, len(secret.Data))
	for k, v := range secret.Data {
		data[k] = v
	}

	return data, nil
}

func (s *service) GetString(ctx context.Context, secretPath, field string) (string, error) {
	field = strings.TrimSpace(field)
	if field == "" {
		return "", merrors.InvalidArgument(s.component+": field is required", "field")
	}

	data, err := s.Get(ctx, secretPath)
	if err != nil {
		return "", err
	}

	val, ok := data[field]
	if !ok {
		return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " missing " + field)
	}

	str, ok := val.(string)
	if !ok || strings.TrimSpace(str) == "" {
		return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " invalid " + field)
	}

	return str, nil
}

func normalizePath(secretPath string) (string, error) {
	normalizedPath := strings.Trim(strings.TrimPrefix(strings.TrimSpace(secretPath), "/"), "/")
	if normalizedPath == "" {
		return "", merrors.InvalidArgument("secret path is required", "secret_path")
	}
	return normalizedPath, nil
}

func resolveToken(config Config) (string, string, error) {
	tokenEnv := strings.TrimSpace(config.TokenEnv)
	if tokenEnv != "" {
		if token := strings.TrimSpace(os.Getenv(tokenEnv)); token != "" {
			return token, "token_env:" + tokenEnv, nil
		}
	}

	tokenFilePath := strings.TrimSpace(config.TokenFile)
	if tokenFileEnv := strings.TrimSpace(config.TokenFileEnv); tokenFileEnv != "" {
		if resolved := strings.TrimSpace(os.Getenv(tokenFileEnv)); resolved != "" {
			tokenFilePath = resolved
		}
	}
	if tokenFilePath != "" {
		//nolint:gosec // Token file path comes from trusted deployment configuration.
		raw, err := os.ReadFile(tokenFilePath)
		if err != nil {
			return "", "", merrors.Internal("vault kv: failed to read token file " + tokenFilePath + ": " + err.Error())
		}
		return strings.TrimSpace(string(raw)), "token_file:" + tokenFilePath, nil
	}

	if tokenEnv != "" {
		return "", "token_env:" + tokenEnv, merrors.InvalidArgument("vault kv: token env " + tokenEnv + " is empty")
	}
	if strings.TrimSpace(config.TokenFileEnv) != "" {
		return "", "token_file_env:" + strings.TrimSpace(config.TokenFileEnv), merrors.InvalidArgument("vault kv: token file env is empty")
	}

	return "", "", merrors.InvalidArgument("vault kv: either token_env or token_file/token_file_env must be configured")
}

var _ Client = (*service)(nil)
