package auth

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

	"github.com/jfraeys/fetch_ml/internal/fileutil"
	"github.com/zalando/go-keyring"
)

// KeychainManager provides secure storage for API keys.
type KeychainManager struct {
	primary  systemKeyring
	fallback *fileKeyStore
}

// systemKeyring abstracts go-keyring for easier testing.
type systemKeyring interface {
	Set(service, account, secret string) error
	Get(service, account string) (string, error)
	Delete(service, account string) error
}

type goKeyring struct{}

func (goKeyring) Set(service, account, secret string) error {
	return keyring.Set(service, account, secret)
}

func (goKeyring) Get(service, account string) (string, error) {
	return keyring.Get(service, account)
}

func (goKeyring) Delete(service, account string) error {
	return keyring.Delete(service, account)
}

// NewKeychainManager returns a manager backed by the OS keyring with a secure file fallback.
func NewKeychainManager() *KeychainManager {
	return newKeychainManagerWithKeyring(goKeyring{}, defaultFallbackDir())
}

func newKeychainManagerWithKeyring(kr systemKeyring, fallbackDir string) *KeychainManager {
	if fallbackDir == "" {
		fallbackDir = defaultFallbackDir()
	}
	return &KeychainManager{
		primary:  kr,
		fallback: newFileKeyStore(fallbackDir),
	}
}

func defaultFallbackDir() string {
	home, err := os.UserHomeDir()
	if err != nil || home == "" {
		return filepath.Join(os.TempDir(), "fetch_ml", "keys")
	}
	return filepath.Join(home, ".fetch_ml", "keys")
}

// StoreAPIKey stores the key in the OS keyring, falling back to a protected file when needed.
func (km *KeychainManager) StoreAPIKey(service, account, key string) error {
	if err := km.primary.Set(service, account, key); err == nil {
		return nil
	} else if errors.Is(err, keyring.ErrUnsupportedPlatform) || errors.Is(err, keyring.ErrNotFound) {
		return km.fallback.store(service, account, key)
	} else if fallbackErr := km.fallback.store(service, account, key); fallbackErr == nil {
		return nil
	}
	return fmt.Errorf("failed to store API key")
}

// GetAPIKey retrieves a key from the OS keyring or fallback store.
func (km *KeychainManager) GetAPIKey(service, account string) (string, error) {
	secret, err := km.primary.Get(service, account)
	if err == nil {
		return secret, nil
	}
	if errors.Is(err, keyring.ErrUnsupportedPlatform) || errors.Is(err, keyring.ErrNotFound) {
		return km.fallback.get(service, account)
	}
	// Unknown error - try fallback before surfacing
	if fallbackSecret, ferr := km.fallback.get(service, account); ferr == nil {
		return fallbackSecret, nil
	}
	return "", fmt.Errorf("failed to retrieve API key")
}

// DeleteAPIKey removes a key from both stores.
func (km *KeychainManager) DeleteAPIKey(service, account string) error {
	// Try to delete from primary keyring, but don't fail on keyring errors
	// (e.g., dbus unavailable, permission denied) - just clean up fallback
	_ = km.primary.Delete(service, account)

	// Always clean up fallback
	if err := km.fallback.delete(service, account); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}

// IsAvailable reports whether the OS keyring backend is usable.
func (km *KeychainManager) IsAvailable() bool {
	_, err := km.primary.Get("fetch_ml_probe", fmt.Sprintf("probe_%d", time.Now().UnixNano()))
	return err == nil || !errors.Is(err, keyring.ErrUnsupportedPlatform)
}

// ListAvailableMethods returns backends the manager can use.
func (km *KeychainManager) ListAvailableMethods() []string {
	methods := []string{}
	if km.IsAvailable() {
		methods = append(methods, "OS keyring")
	}
	methods = append(methods, fmt.Sprintf("Encrypted file (%s)", km.fallback.baseDir))
	return methods
}

// fileKeyStore stores secrets with 0600 permissions as a fallback.
type fileKeyStore struct {
	baseDir string
	mu      sync.Mutex
}

func newFileKeyStore(baseDir string) *fileKeyStore {
	return &fileKeyStore{baseDir: baseDir}
}

func (f *fileKeyStore) store(service, account, secret string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if err := os.MkdirAll(f.baseDir, 0o700); err != nil {
		return fmt.Errorf("failed to prepare key store: %w", err)
	}
	path := f.path(service, account)
	// SECURITY: Write with fsync for crash safety
	return fileutil.WriteFileSafe(path, []byte(secret), 0o600)
}

func (f *fileKeyStore) get(service, account string) (string, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	data, err := os.ReadFile(f.path(service, account))
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func (f *fileKeyStore) delete(service, account string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	path := f.path(service, account)
	if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}

func (f *fileKeyStore) path(service, account string) string {
	return filepath.Join(f.baseDir, fmt.Sprintf("%s_%s.key", sanitize(service), sanitize(account)))
}

func sanitize(value string) string {
	replacer := strings.NewReplacer("/", "_", "\\", "_", "..", "_", " ", "_", "\t", "_")
	return replacer.Replace(value)
}
