package smb

import (
	"fmt"
	"os"
	"os/user"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/jcmturner/gokrb5/v8/client"
	"github.com/jcmturner/gokrb5/v8/config"
	"github.com/jcmturner/gokrb5/v8/credentials"
)

// KerberosFactory encapsulates dependencies and caches for Kerberos clients.
type KerberosFactory struct {
	// clientCache caches Kerberos clients keyed by resolved ccache path.
	// Clients are reused unless the associated ccache file changes.
	clientCache sync.Map // map[string]*client.Client

	// errCache caches errors encountered when loading Kerberos clients.
	// Prevents repeated attempts for paths that previously failed.
	errCache sync.Map // map[string]error

	// modTimeCache tracks the last known modification time of ccache files.
	// Used to detect changes and trigger credential refresh.
	modTimeCache sync.Map // map[string]time.Time

	loadCCache func(string) (*credentials.CCache, error)
	newClient  func(*credentials.CCache, *config.Config, ...func(*client.Settings)) (*client.Client, error)
	loadConfig func() (*config.Config, error)
}

// kerberosFactory is the shared KerberosFactory used to cache all connections
var kerberosFactory = NewKerberosFactory()

// NewKerberosFactory creates a new instance of KerberosFactory with default dependencies.
func NewKerberosFactory() *KerberosFactory {
	return &KerberosFactory{
		loadCCache: credentials.LoadCCache,
		newClient:  client.NewFromCCache,
		loadConfig: defaultLoadKerberosConfig,
	}
}

// GetClient returns a cached Kerberos client or creates a new one if needed.
func (kf *KerberosFactory) GetClient(ccachePath string) (*client.Client, error) {
	resolvedPath, err := resolveCcachePath(ccachePath)
	if err != nil {
		return nil, err
	}

	stat, err := os.Stat(resolvedPath)
	if err != nil {
		kf.errCache.Store(resolvedPath, err)
		return nil, err
	}
	mtime := stat.ModTime()

	if oldMod, ok := kf.modTimeCache.Load(resolvedPath); ok {
		if oldTime, ok := oldMod.(time.Time); ok && oldTime.Equal(mtime) {
			if errVal, ok := kf.errCache.Load(resolvedPath); ok {
				return nil, errVal.(error)
			}
			if clientVal, ok := kf.clientCache.Load(resolvedPath); ok {
				return clientVal.(*client.Client), nil
			}
		}
	}

	// Load Kerberos config
	cfg, err := kf.loadConfig()
	if err != nil {
		kf.errCache.Store(resolvedPath, err)
		return nil, err
	}

	// Load ccache
	ccache, err := kf.loadCCache(resolvedPath)
	if err != nil {
		kf.errCache.Store(resolvedPath, err)
		return nil, err
	}

	// Create new client
	cl, err := kf.newClient(ccache, cfg)
	if err != nil {
		kf.errCache.Store(resolvedPath, err)
		return nil, err
	}

	// Cache and return
	kf.clientCache.Store(resolvedPath, cl)
	kf.errCache.Delete(resolvedPath)
	kf.modTimeCache.Store(resolvedPath, mtime)
	return cl, nil
}

// resolveCcachePath resolves the KRB5 ccache path.
func resolveCcachePath(ccachePath string) (string, error) {
	if ccachePath == "" {
		ccachePath = os.Getenv("KRB5CCNAME")
	}

	switch {
	case strings.Contains(ccachePath, ":"):
		parts := strings.SplitN(ccachePath, ":", 2)
		prefix, path := parts[0], parts[1]
		switch prefix {
		case "FILE":
			return path, nil
		case "DIR":
			primary, err := os.ReadFile(filepath.Join(path, "primary"))
			if err != nil {
				return "", err
			}
			return filepath.Join(path, strings.TrimSpace(string(primary))), nil
		default:
			return "", fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath)
		}
	case ccachePath == "":
		u, err := user.Current()
		if err != nil {
			return "", err
		}
		return "/tmp/krb5cc_" + u.Uid, nil
	default:
		return ccachePath, nil
	}
}

// defaultLoadKerberosConfig loads Kerberos config from default or env path.
func defaultLoadKerberosConfig() (*config.Config, error) {
	cfgPath := os.Getenv("KRB5_CONFIG")
	if cfgPath == "" {
		cfgPath = "/etc/krb5.conf"
	}
	return config.Load(cfgPath)
}
