package core

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"os"
	"path/filepath"
	"sync"

	"github.com/zalando/go-keyring"
)

// CredentialStore is the REQ-2.3.5 secret-storage abstraction every auth
// strategy (Story G4) resolves secrets through.
type CredentialStore interface {
	Save(key, secret string) error
	Get(key string) (string, bool, error)
	Delete(key string) error
}

const keyringService = "{{ project_name }}"

// KeyringStore wraps `zalando/go-keyring` — Go's direct equivalent of
// Node's `keytar`/Python's `keyring` — for native OS-keychain integration
// (macOS Keychain, Windows Credential Manager, Linux Secret Service).
type KeyringStore struct{}

func (KeyringStore) Save(key, secret string) error {
	return keyring.Set(keyringService, key, secret)
}

func (KeyringStore) Get(key string) (value string, found bool, err error) {
	value, err = keyring.Get(keyringService, key)
	if err == keyring.ErrNotFound {
		return "", false, nil
	}
	if err != nil {
		return "", false, err
	}
	return value, true, nil
}

func (KeyringStore) Delete(key string) error {
	err := keyring.Delete(keyringService, key)
	if err == keyring.ErrNotFound {
		return nil
	}
	return err
}

// EncryptedFileStore is REQ-2.3.5's fallback: secrets are AES-256-GCM
// encrypted at rest under `~/.{{ project_name }}/credentials.enc`, keyed by
// a per-installation key generated on first use and stored alongside it —
// the same shape as `targets::csharp::templates::Core::CredentialStorage.cs.tera`'s
// `EncryptedFileCredentialStore`, needed here for the same reason: no
// keychain/Secret Service backend is guaranteed to be present (containers
// and CI runners commonly lack one, unlike a developer's own machine).
type EncryptedFileStore struct {
	dir string
	mu  sync.Mutex
}

func NewEncryptedFileStore() (*EncryptedFileStore, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}
	dir := filepath.Join(home, ".{{ project_name }}")
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return nil, err
	}
	return &EncryptedFileStore{dir: dir}, nil
}

func (s *EncryptedFileStore) credentialsPath() string { return filepath.Join(s.dir, "credentials.enc") }
func (s *EncryptedFileStore) keyPath() string         { return filepath.Join(s.dir, "credentials.key") }

func (s *EncryptedFileStore) Save(key, secret string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	store, err := s.readAll()
	if err != nil {
		return err
	}
	store[key] = secret
	return s.writeAll(store)
}

func (s *EncryptedFileStore) Get(key string) (value string, found bool, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	store, err := s.readAll()
	if err != nil {
		return "", false, err
	}
	value, found = store[key]
	return value, found, nil
}

func (s *EncryptedFileStore) Delete(key string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	store, err := s.readAll()
	if err != nil {
		return err
	}
	if _, ok := store[key]; !ok {
		return nil
	}
	delete(store, key)
	return s.writeAll(store)
}

func (s *EncryptedFileStore) readAll() (map[string]string, error) {
	encrypted, err := os.ReadFile(s.credentialsPath())
	if os.IsNotExist(err) {
		return map[string]string{}, nil
	}
	if err != nil {
		return nil, err
	}

	key, err := s.getOrCreateKey()
	if err != nil {
		return nil, err
	}
	plaintext, err := decrypt(encrypted, key)
	if err != nil {
		return nil, err
	}
	store := map[string]string{}
	if err := json.Unmarshal(plaintext, &store); err != nil {
		return nil, err
	}
	return store, nil
}

func (s *EncryptedFileStore) writeAll(store map[string]string) error {
	key, err := s.getOrCreateKey()
	if err != nil {
		return err
	}
	plaintext, err := json.Marshal(store)
	if err != nil {
		return err
	}
	encrypted, err := encrypt(plaintext, key)
	if err != nil {
		return err
	}
	return os.WriteFile(s.credentialsPath(), encrypted, 0o600)
}

func (s *EncryptedFileStore) getOrCreateKey() ([]byte, error) {
	encoded, err := os.ReadFile(s.keyPath())
	if err == nil {
		return base64.StdEncoding.DecodeString(string(encoded))
	}
	if !os.IsNotExist(err) {
		return nil, err
	}

	key := make([]byte, 32)
	if _, err := rand.Read(key); err != nil {
		return nil, err
	}
	if err := os.WriteFile(s.keyPath(), []byte(base64.StdEncoding.EncodeToString(key)), 0o600); err != nil {
		return nil, err
	}
	return key, nil
}

func encrypt(plaintext, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, plaintext, nil), nil
}

func decrypt(data, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonceSize := gcm.NonceSize()
	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
	return gcm.Open(nil, nonce, ciphertext, nil)
}

// FallbackStore tries the OS keychain first and falls back to an
// encrypted file store when no keychain/Secret Service backend is
// available (REQ-2.3.5) — the common case in containers/CI, where
// `zalando/go-keyring`'s Linux backend (D-Bus Secret Service) has nothing
// to talk to.
type FallbackStore struct {
	primary  CredentialStore
	fallback CredentialStore
}

func NewFallbackStore(primary, fallback CredentialStore) *FallbackStore {
	return &FallbackStore{primary: primary, fallback: fallback}
}

func (s *FallbackStore) Save(key, secret string) error {
	if err := s.primary.Save(key, secret); err == nil {
		return nil
	}
	return s.fallback.Save(key, secret)
}

func (s *FallbackStore) Get(key string) (value string, found bool, err error) {
	if value, found, err := s.primary.Get(key); err == nil {
		return value, found, nil
	}
	return s.fallback.Get(key)
}

func (s *FallbackStore) Delete(key string) error {
	if err := s.primary.Delete(key); err == nil {
		return nil
	}
	return s.fallback.Delete(key)
}

// CachingStore layers an in-memory cache over any CredentialStore so
// repeated Get calls for the same key don't re-hit the keychain/disk on
// every credential lookup within a process lifetime (REQ-2.3.5).
type CachingStore struct {
	inner CredentialStore
	mu    sync.Mutex
	cache map[string]string
}

func NewCachingStore(inner CredentialStore) *CachingStore {
	return &CachingStore{inner: inner, cache: make(map[string]string)}
}

func (s *CachingStore) Save(key, secret string) error {
	if err := s.inner.Save(key, secret); err != nil {
		return err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cache[key] = secret
	return nil
}

func (s *CachingStore) Get(key string) (value string, found bool, err error) {
	s.mu.Lock()
	if cached, ok := s.cache[key]; ok {
		s.mu.Unlock()
		return cached, true, nil
	}
	s.mu.Unlock()

	value, found, err = s.inner.Get(key)
	if err != nil {
		return "", false, err
	}
	if found {
		s.mu.Lock()
		s.cache[key] = value
		s.mu.Unlock()
	}
	return value, found, nil
}

func (s *CachingStore) Delete(key string) error {
	if err := s.inner.Delete(key); err != nil {
		return err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.cache, key)
	return nil
}

// NewCredentialStore builds the REQ-2.3.5 credential store every auth
// strategy (Story G4) resolves secrets through: OS keychain first, an
// encrypted file store as fallback, wrapped in an in-memory cache.
func NewCredentialStore() (CredentialStore, error) {
	fileStore, err := NewEncryptedFileStore()
	if err != nil {
		return nil, err
	}
	return NewCachingStore(NewFallbackStore(KeyringStore{}, fileStore)), nil
}
