// Package storage provides file-based persistence for HMI interface definitions.
package storage

import (
	"encoding/xml"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
	"unicode"
)

// ErrNotFound is returned when the requested interface does not exist.
var ErrNotFound = errors.New("interface not found")

// InterfaceMeta is the lightweight representation returned by List.
type InterfaceMeta struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Version int    `json:"version"`
}

// Store persists interface definitions as XML files under {storageDir}/interfaces/.
type Store struct {
	dir string
}

// New opens (and creates, if needed) the storage directory.
func New(storageDir string) (*Store, error) {
	dir := filepath.Join(storageDir, "interfaces")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, fmt.Errorf("create interfaces dir: %w", err)
	}
	return &Store{dir: dir}, nil
}

// validateID returns an error if id could be used for path traversal or is
// otherwise unsafe.  Valid IDs contain only letters, digits, hyphens, and
// underscores.
func validateID(id string) error {
	if id == "" {
		return fmt.Errorf("interface ID must not be empty")
	}
	for _, r := range id {
		if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' && r != '_' {
			return fmt.Errorf("invalid character %q in interface ID", r)
		}
	}
	return nil
}

func (s *Store) filePath(id string) string {
	return filepath.Join(s.dir, id+".xml")
}

// List returns metadata for every stored interface.
func (s *Store) List() ([]InterfaceMeta, error) {
	entries, err := os.ReadDir(s.dir)
	if err != nil {
		return nil, err
	}
	var out []InterfaceMeta
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".xml") {
			continue
		}
		id := strings.TrimSuffix(e.Name(), ".xml")
		meta, err := s.readMeta(id)
		if err != nil {
			continue // skip corrupt files silently
		}
		out = append(out, meta)
	}
	if out == nil {
		out = []InterfaceMeta{}
	}
	return out, nil
}

// rootAttrs is used to parse only the top-level XML attributes.
type rootAttrs struct {
	XMLName xml.Name `xml:"interface"`
	ID      string   `xml:"id,attr"`
	Name    string   `xml:"name,attr"`
	Version int      `xml:"version,attr"`
}

func (s *Store) readMeta(id string) (InterfaceMeta, error) {
	data, err := os.ReadFile(s.filePath(id))
	if err != nil {
		return InterfaceMeta{}, err
	}
	var root rootAttrs
	if err := xml.Unmarshal(data, &root); err != nil {
		return InterfaceMeta{}, err
	}
	return InterfaceMeta{ID: id, Name: root.Name, Version: root.Version}, nil
}

// Get returns the raw XML bytes for the interface with the given ID.
func (s *Store) Get(id string) ([]byte, error) {
	if err := validateID(id); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrNotFound, err)
	}
	data, err := os.ReadFile(s.filePath(id))
	if errors.Is(err, os.ErrNotExist) {
		return nil, ErrNotFound
	}
	return data, err
}

// Create stores new interface XML and returns the generated ID.
// The ID is derived from the name attribute; a timestamp suffix is added to
// ensure uniqueness.
func (s *Store) Create(xmlData []byte) (string, error) {
	var root rootAttrs
	if err := xml.Unmarshal(xmlData, &root); err != nil {
		return "", fmt.Errorf("invalid XML: %w", err)
	}

	id := root.ID
	if id == "" {
		id = slugify(root.Name)
	}
	// Guarantee uniqueness.
	if _, err := os.Stat(s.filePath(id)); err == nil {
		id = id + "-" + fmt.Sprintf("%d", time.Now().UnixMilli())
	}
	return id, os.WriteFile(s.filePath(id), xmlData, 0o644)
}

// Update replaces the XML for an existing interface.
func (s *Store) Update(id string, xmlData []byte) error {
	if err := validateID(id); err != nil {
		return ErrNotFound
	}
	if _, err := os.Stat(s.filePath(id)); errors.Is(err, os.ErrNotExist) {
		return ErrNotFound
	}
	return os.WriteFile(s.filePath(id), xmlData, 0o644)
}

// Delete removes the interface with the given ID.
func (s *Store) Delete(id string) error {
	if err := validateID(id); err != nil {
		return ErrNotFound
	}
	err := os.Remove(s.filePath(id))
	if errors.Is(err, os.ErrNotExist) {
		return ErrNotFound
	}
	return err
}

// Clone duplicates an interface, assigning a new unique ID.
func (s *Store) Clone(srcID string) (string, error) {
	if err := validateID(srcID); err != nil {
		return "", ErrNotFound
	}
	data, err := s.Get(srcID)
	if err != nil {
		return "", err
	}
	newID := srcID + "-copy-" + fmt.Sprintf("%d", time.Now().UnixMilli())
	return newID, os.WriteFile(s.filePath(newID), data, 0o644)
}

// slugify converts a human-readable name into a URL-safe identifier.
func slugify(s string) string {
	var b strings.Builder
	for _, r := range strings.ToLower(s) {
		switch {
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
			b.WriteRune(r)
		default:
			if b.Len() > 0 && b.String()[b.Len()-1] != '-' {
				b.WriteByte('-')
			}
		}
	}
	result := strings.Trim(b.String(), "-")
	if result == "" {
		return "interface"
	}
	return result
}
