package controllogic

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

const definitionsFile = "controllogic.json"

// ErrNotFound is returned when a graph id does not exist.
var ErrNotFound = errors.New("control logic graph not found")

// Store persists control-logic graphs as a single JSON file in the storage dir.
// Writes are atomic (tmp file + rename); all access is mutex-guarded.
//
// Git-style versioning: the live graph lives in controllogic.json (the current
// revision), while every superseded revision is preserved as a backup file
// {id}.vN.json under versionsDir. Promote/Fork build on these backups.
type Store struct {
	mu          sync.RWMutex
	path        string
	trashDir    string
	versionsDir string
	items       map[string]Graph
}

// NewStore opens (or initialises) the control-logic store under storageDir.
func NewStore(storageDir string) (*Store, error) {
	s := &Store{
		path:        filepath.Join(storageDir, definitionsFile),
		trashDir:    filepath.Join(storageDir, "trash", "controllogic"),
		versionsDir: filepath.Join(storageDir, "controllogic_versions"),
		items:       map[string]Graph{},
	}
	if err := s.load(); err != nil {
		return nil, err
	}
	return s, nil
}

func (s *Store) load() error {
	data, err := os.ReadFile(s.path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	var graphs []Graph
	if err := json.Unmarshal(data, &graphs); err != nil {
		return fmt.Errorf("parse %s: %w", s.path, err)
	}
	for _, g := range graphs {
		s.items[g.ID] = g
	}
	return nil
}

// saveLocked writes the current set atomically. Caller must hold s.mu.
func (s *Store) saveLocked() error {
	graphs := make([]Graph, 0, len(s.items))
	for _, g := range s.items {
		graphs = append(graphs, g)
	}
	data, err := json.MarshalIndent(graphs, "", "  ")
	if err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, s.path)
}

// List returns all graphs.
func (s *Store) List() []Graph {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]Graph, 0, len(s.items))
	for _, g := range s.items {
		out = append(out, g)
	}
	return out
}

// Get returns a single graph by id.
func (s *Store) Get(id string) (Graph, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	g, ok := s.items[id]
	if !ok {
		return Graph{}, ErrNotFound
	}
	return g, nil
}

// Save inserts or replaces a graph and persists the store. When replacing an
// existing graph the superseded revision is backed up as {id}.vN.json and the
// new graph's Version is bumped; a brand-new graph starts at version 1.
func (s *Store) Save(g Graph) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if old, ok := s.items[g.ID]; ok {
		oldV := old.Version
		if oldV < 1 {
			oldV = 1
			old.Version = 1
		}
		if err := s.backupLocked(old); err != nil {
			return fmt.Errorf("back up control logic revision: %w", err)
		}
		g.Version = oldV + 1
	} else if g.Version < 1 {
		g.Version = 1
	}
	s.items[g.ID] = g
	return s.saveLocked()
}

// backupLocked writes a single graph revision to versionsDir as {id}.vN.json.
// Caller must hold s.mu.
func (s *Store) backupLocked(g Graph) error {
	if err := os.MkdirAll(s.versionsDir, 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(g, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.versionPath(g.ID, g.Version), data, 0o644)
}

func (s *Store) versionPath(id string, version int) string {
	return filepath.Join(s.versionsDir, fmt.Sprintf("%s.v%d.json", id, version))
}

// Delete removes a graph by id, first writing a copy into the trash folder so it
// can be recovered if needed. The trash backup is best-effort: a failure to write
// it does not prevent the delete (but is reported).
func (s *Store) Delete(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	g, ok := s.items[id]
	if !ok {
		return ErrNotFound
	}
	if err := s.trash(g); err != nil {
		return fmt.Errorf("move control logic to trash: %w", err)
	}
	delete(s.items, id)
	return s.saveLocked()
}

// trash writes a single graph as a timestamped JSON file under the trash folder.
func (s *Store) trash(g Graph) error {
	if err := os.MkdirAll(s.trashDir, 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(g, "", "  ")
	if err != nil {
		return err
	}
	dst := filepath.Join(s.trashDir, fmt.Sprintf("%s.%d.json", g.ID, time.Now().UnixMilli()))
	return os.WriteFile(dst, data, 0o644)
}
