package secrets

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"sync"
	"time"
)

type Record struct {
	ID         string    `json:"id"`
	Type       string    `json:"type"`
	Nonce      string    `json:"nonce"`
	Ciphertext string    `json:"ciphertext"`
	UpdatedAt  time.Time `json:"updatedAt"`
}

type Store struct {
	dir string
	mu  sync.Mutex
}

var validID = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)

func New(dir string) *Store { return &Store{dir: dir} }

func (s *Store) Init() error {
	if err := os.MkdirAll(s.dir, 0700); err != nil {
		return err
	}
	keyPath := filepath.Join(s.dir, "secret.key")
	if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) {
		key := make([]byte, 32)
		if _, err := io.ReadFull(rand.Reader, key); err != nil {
			return err
		}
		return os.WriteFile(keyPath, key, 0600)
	}
	return nil
}

func (s *Store) Put(id, kind, plaintext string) error {
	if !validID.MatchString(id) || kind == "" {
		return errors.New("invalid secret id or type")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	key, err := s.key()
	if err != nil {
		return err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return err
	}
	sealed := gcm.Seal(nil, nonce, []byte(plaintext), []byte(id+":"+kind))
	record := Record{ID: id, Type: kind, Nonce: base64.StdEncoding.EncodeToString(nonce), Ciphertext: base64.StdEncoding.EncodeToString(sealed), UpdatedAt: time.Now().UTC()}
	b, err := json.Marshal(record)
	if err != nil {
		return err
	}
	return os.WriteFile(filepath.Join(s.dir, id+".json"), append(b, '\n'), 0600)
}

func (s *Store) Get(id string) (string, error) {
	if !validID.MatchString(id) {
		return "", errors.New("invalid secret id")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	b, err := os.ReadFile(filepath.Join(s.dir, id+".json"))
	if err != nil {
		return "", err
	}
	var record Record
	if err := json.Unmarshal(b, &record); err != nil {
		return "", err
	}
	key, err := s.key()
	if err != nil {
		return "", err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	nonce, err := base64.StdEncoding.DecodeString(record.Nonce)
	if err != nil {
		return "", err
	}
	sealed, err := base64.StdEncoding.DecodeString(record.Ciphertext)
	if err != nil {
		return "", err
	}
	plain, err := gcm.Open(nil, nonce, sealed, []byte(record.ID+":"+record.Type))
	if err != nil {
		return "", err
	}
	return string(plain), nil
}

func (s *Store) Delete(id string) error {
	if !validID.MatchString(id) {
		return errors.New("invalid secret id")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	err := os.Remove(filepath.Join(s.dir, id+".json"))
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	return err
}

func (s *Store) key() ([]byte, error) {
	key, err := os.ReadFile(filepath.Join(s.dir, "secret.key"))
	if err != nil {
		return nil, err
	}
	if len(key) != 32 {
		return nil, errors.New("invalid secret key length")
	}
	return key, nil
}
