package confmgr

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"
)

// ErrNotFound is returned when the requested set or instance does not exist.
var ErrNotFound = errors.New("config object not found")

// Kind selects which collection a store operation targets.
type Kind int

const (
	KindSet Kind = iota
	KindInstance
	KindRule
)

func (k Kind) sub() string {
	switch k {
	case KindInstance:
		return "instances"
	case KindRule:
		return "rules"
	default:
		return "sets"
	}
}

// Meta is the lightweight listing representation.
type Meta struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Version int    `json:"version"`
	// SetID is only populated for instances (the schema they belong to); it is
	// empty for sets. Lets clients group/filter instances by set without a GET
	// per instance.
	SetID string `json:"setId,omitempty"`
	// Enabled is only populated for rules: nil for sets/instances and for legacy
	// rules without the flag. Lets the rule list show enabled/disabled status
	// without a GET per rule.
	Enabled *bool `json:"enabled,omitempty"`
	// Owner/Scope/Groups carry the visibility metadata so list handlers can filter
	// by the caller without a GET per object. Empty scope = global (legacy-safe).
	Owner  string   `json:"owner,omitempty"`
	Scope  string   `json:"scope,omitempty"`
	Groups []string `json:"groups,omitempty"`
}

// VersionMeta describes a single persisted revision.
type VersionMeta struct {
	Version int       `json:"version"`
	Name    string    `json:"name"`
	Tag     string    `json:"tag,omitempty"`
	Current bool      `json:"current"`
	SavedAt time.Time `json:"savedAt"`
}

// Store persists configuration sets and instances as versioned JSON files
// under {storageDir}/configs/{sets,instances}/. Each object lives in
// {id}.json with prior revisions preserved as {id}.v{N}.json backups, exactly
// mirroring the panel storage versioning scheme.
type Store struct {
	rootDir string
	dirs    map[Kind]string
}

// New opens (and creates, if needed) the configs storage directories.
func New(storageDir string) (*Store, error) {
	s := &Store{rootDir: storageDir, dirs: map[Kind]string{}}
	for _, k := range []Kind{KindSet, KindInstance, KindRule} {
		dir := filepath.Join(storageDir, "configs", k.sub())
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return nil, fmt.Errorf("create %s dir: %w", k.sub(), err)
		}
		s.dirs[k] = dir
	}
	return s, nil
}

// header parses the metadata fields shared by sets and instances.
type header struct {
	ID      string   `json:"id"`
	Name    string   `json:"name"`
	Version int      `json:"version"`
	Tag     string   `json:"tag"`
	SetID   string   `json:"setId"`
	Enabled *bool    `json:"enabled"`
	Owner   string   `json:"owner"`
	Scope   string   `json:"scope"`
	Groups  []string `json:"groups"`
}

func validateID(id string) error {
	if id == "" {
		return fmt.Errorf("config 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 config ID", r)
		}
	}
	return nil
}

func (s *Store) filePath(k Kind, id string) string {
	return filepath.Join(s.dirs[k], id+".json")
}

func (s *Store) backupPath(k Kind, id string, version int) string {
	return filepath.Join(s.dirs[k], fmt.Sprintf("%s.v%d.json", id, version))
}

// isVersioned reports whether name matches <id>.v<number>.json.
func isVersioned(name string) bool {
	parts := strings.Split(name, ".")
	if len(parts) != 3 || parts[2] != "json" {
		return false
	}
	if !strings.HasPrefix(parts[1], "v") {
		return false
	}
	_, err := strconv.Atoi(parts[1][1:])
	return err == nil
}

func readHeader(data []byte) (header, error) {
	var h header
	if err := json.Unmarshal(data, &h); err != nil {
		return header{}, err
	}
	return h, nil
}

// List returns metadata for every stored object of the given kind.
func (s *Store) List(k Kind) ([]Meta, error) {
	entries, err := os.ReadDir(s.dirs[k])
	if err != nil {
		return nil, err
	}
	out := []Meta{}
	for _, e := range entries {
		name := e.Name()
		if e.IsDir() || !strings.HasSuffix(name, ".json") || isVersioned(name) {
			continue
		}
		id := strings.TrimSuffix(name, ".json")
		data, err := os.ReadFile(s.filePath(k, id))
		if err != nil {
			continue
		}
		h, err := readHeader(data)
		if err != nil {
			continue
		}
		out = append(out, Meta{ID: id, Name: h.Name, Version: h.Version, SetID: h.SetID, Enabled: h.Enabled, Owner: h.Owner, Scope: h.Scope, Groups: h.Groups})
	}
	return out, nil
}

// get returns the raw JSON bytes for the current revision.
func (s *Store) get(k Kind, id string) ([]byte, error) {
	if err := validateID(id); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrNotFound, err)
	}
	data, err := os.ReadFile(s.filePath(k, id))
	if errors.Is(err, os.ErrNotExist) {
		return nil, ErrNotFound
	}
	return data, err
}

// create writes a new object, deriving a unique ID from name when obj has none,
// stamping version=1 and the given tag. It returns the raw stored bytes.
func (s *Store) create(k Kind, obj map[string]any, tag string) (string, []byte, error) {
	id, _ := obj["id"].(string)
	if id == "" {
		id = slugify(name(obj))
	}
	if err := validateID(id); err != nil {
		return "", nil, err
	}
	if _, err := os.Stat(s.filePath(k, id)); err == nil {
		id = id + "-" + strconv.FormatInt(time.Now().UnixMilli(), 10)
	}
	obj["id"] = id
	obj["version"] = 1
	if tag != "" {
		obj["tag"] = tag
	}
	data, err := marshal(obj)
	if err != nil {
		return "", nil, err
	}
	return id, data, os.WriteFile(s.filePath(k, id), data, 0o644)
}

// update replaces the current revision, preserving the prior one as a backup
// and incrementing the version. It returns the raw stored bytes.
func (s *Store) update(k Kind, id string, obj map[string]any, tag string) ([]byte, error) {
	if err := validateID(id); err != nil {
		return nil, ErrNotFound
	}
	oldData, err := os.ReadFile(s.filePath(k, id))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	oldHdr, err := readHeader(oldData)
	if err != nil {
		return nil, fmt.Errorf("parse existing JSON: %w", err)
	}
	if oldHdr.Version < 1 {
		oldHdr.Version = 1
	}
	if err := os.WriteFile(s.backupPath(k, id, oldHdr.Version), oldData, 0o644); err != nil {
		return nil, fmt.Errorf("create backup: %w", err)
	}
	obj["id"] = id
	obj["version"] = oldHdr.Version + 1
	if tag != "" {
		obj["tag"] = tag
	}
	data, err := marshal(obj)
	if err != nil {
		return nil, err
	}
	return data, os.WriteFile(s.filePath(k, id), data, 0o644)
}

// Versions returns metadata for every persisted revision, newest-first.
func (s *Store) Versions(k Kind, id string) ([]VersionMeta, error) {
	if err := validateID(id); err != nil {
		return nil, ErrNotFound
	}
	curInfo, err := os.Stat(s.filePath(k, id))
	if errors.Is(err, os.ErrNotExist) {
		return nil, ErrNotFound
	} else if err != nil {
		return nil, err
	}
	curData, err := os.ReadFile(s.filePath(k, id))
	if err != nil {
		return nil, err
	}
	curHdr, err := readHeader(curData)
	if err != nil {
		return nil, err
	}
	out := []VersionMeta{{
		Version: curHdr.Version,
		Name:    curHdr.Name,
		Tag:     curHdr.Tag,
		Current: true,
		SavedAt: curInfo.ModTime(),
	}}

	entries, err := os.ReadDir(s.dirs[k])
	if err != nil {
		return nil, err
	}
	prefix := id + ".v"
	for _, e := range entries {
		name := e.Name()
		if e.IsDir() || !strings.HasPrefix(name, prefix) || !isVersioned(name) {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		data, err := os.ReadFile(filepath.Join(s.dirs[k], name))
		if err != nil {
			continue
		}
		h, err := readHeader(data)
		if err != nil {
			continue
		}
		out = append(out, VersionMeta{
			Version: h.Version,
			Name:    h.Name,
			Tag:     h.Tag,
			SavedAt: info.ModTime(),
		})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Version > out[j].Version })
	return out, nil
}

// getVersion returns the raw JSON bytes for a specific revision.
func (s *Store) getVersion(k Kind, id string, version int) ([]byte, error) {
	if err := validateID(id); err != nil {
		return nil, ErrNotFound
	}
	curData, err := os.ReadFile(s.filePath(k, id))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if h, err := readHeader(curData); err == nil && h.Version == version {
		return curData, nil
	}
	data, err := os.ReadFile(s.backupPath(k, id, version))
	if errors.Is(err, os.ErrNotExist) {
		return nil, ErrNotFound
	}
	return data, err
}

// Promote re-saves a past revision as a new revision on top of history.
func (s *Store) Promote(k Kind, id string, version int) error {
	data, err := s.getVersion(k, id, version)
	if err != nil {
		return err
	}
	obj, err := unmarshal(data)
	if err != nil {
		return err
	}
	_, err = s.update(k, id, obj, fmt.Sprintf("restored from v%d", version))
	return err
}

// Fork creates a brand-new object from a revision, with a fresh ID and version 1.
func (s *Store) Fork(k Kind, id string, version int) (string, error) {
	data, err := s.getVersion(k, id, version)
	if err != nil {
		return "", err
	}
	obj, err := unmarshal(data)
	if err != nil {
		return "", err
	}
	newID := id + "-fork-" + strconv.FormatInt(time.Now().UnixMilli(), 10)
	obj["id"] = newID
	obj["version"] = 1
	delete(obj, "tag")
	out, err := marshal(obj)
	if err != nil {
		return "", err
	}
	return newID, os.WriteFile(s.filePath(k, newID), out, 0o644)
}

// Delete moves an object and all its backups to a timestamped trash folder so
// it can be recovered. Nothing is destroyed.
func (s *Store) Delete(k Kind, id string) error {
	if err := validateID(id); err != nil {
		return ErrNotFound
	}
	if _, err := os.Stat(s.filePath(k, id)); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ErrNotFound
		}
		return err
	}
	trashDir := filepath.Join(s.rootDir, "trash", "configs", k.sub(), fmt.Sprintf("%s.%d", id, time.Now().UnixMilli()))
	if err := os.MkdirAll(trashDir, 0o755); err != nil {
		return fmt.Errorf("create trash dir: %w", err)
	}
	entries, err := os.ReadDir(s.dirs[k])
	if err != nil {
		return err
	}
	for _, e := range entries {
		name := e.Name()
		if e.IsDir() {
			continue
		}
		if name == id+".json" || (strings.HasPrefix(name, id+".v") && isVersioned(name)) {
			if err := os.Rename(filepath.Join(s.dirs[k], name), filepath.Join(trashDir, name)); err != nil {
				return fmt.Errorf("move %s to trash: %w", name, err)
			}
		}
	}
	return nil
}

// ── typed wrappers ──────────────────────────────────────────────────────────

// GetSet returns the current revision of a config set.
func (s *Store) GetSet(id string) (ConfigSet, error) {
	data, err := s.get(KindSet, id)
	if err != nil {
		return ConfigSet{}, err
	}
	var set ConfigSet
	return set, json.Unmarshal(data, &set)
}

// GetSetVersion returns a specific revision of a config set.
func (s *Store) GetSetVersion(id string, version int) (ConfigSet, error) {
	data, err := s.getVersion(KindSet, id, version)
	if err != nil {
		return ConfigSet{}, err
	}
	var set ConfigSet
	return set, json.Unmarshal(data, &set)
}

// CreateSet validates and stores a new config set, returning it with its
// assigned ID and version.
func (s *Store) CreateSet(set ConfigSet, tag string) (ConfigSet, error) {
	if err := set.Validate(); err != nil {
		return ConfigSet{}, err
	}
	obj, err := toMap(set)
	if err != nil {
		return ConfigSet{}, err
	}
	_, data, err := s.create(KindSet, obj, tag)
	if err != nil {
		return ConfigSet{}, err
	}
	var out ConfigSet
	return out, json.Unmarshal(data, &out)
}

// UpdateSet validates and stores a new revision of an existing config set.
func (s *Store) UpdateSet(id string, set ConfigSet, tag string) (ConfigSet, error) {
	if err := set.Validate(); err != nil {
		return ConfigSet{}, err
	}
	obj, err := toMap(set)
	if err != nil {
		return ConfigSet{}, err
	}
	data, err := s.update(KindSet, id, obj, tag)
	if err != nil {
		return ConfigSet{}, err
	}
	var out ConfigSet
	return out, json.Unmarshal(data, &out)
}

// GetInstance returns the current revision of a config instance.
func (s *Store) GetInstance(id string) (ConfigInstance, error) {
	data, err := s.get(KindInstance, id)
	if err != nil {
		return ConfigInstance{}, err
	}
	var inst ConfigInstance
	return inst, json.Unmarshal(data, &inst)
}

// GetInstanceVersion returns a specific revision of a config instance.
func (s *Store) GetInstanceVersion(id string, version int) (ConfigInstance, error) {
	data, err := s.getVersion(KindInstance, id, version)
	if err != nil {
		return ConfigInstance{}, err
	}
	var inst ConfigInstance
	return inst, json.Unmarshal(data, &inst)
}

// setForInstance loads the schema an instance is bound to, honouring a pinned
// SetVersion when set.
func (s *Store) setForInstance(inst ConfigInstance) (ConfigSet, error) {
	if inst.SetVersion > 0 {
		return s.GetSetVersion(inst.SetID, inst.SetVersion)
	}
	return s.GetSet(inst.SetID)
}

// CreateInstance validates an instance against its set and stores it.
func (s *Store) CreateInstance(inst ConfigInstance, tag string) (ConfigInstance, error) {
	set, err := s.setForInstance(inst)
	if err != nil {
		return ConfigInstance{}, fmt.Errorf("load set %q: %w", inst.SetID, err)
	}
	if err := inst.ValidateAgainst(set); err != nil {
		return ConfigInstance{}, err
	}
	if err := s.applyRules(&inst, set); err != nil {
		return ConfigInstance{}, err
	}
	obj, err := toMap(inst)
	if err != nil {
		return ConfigInstance{}, err
	}
	_, data, err := s.create(KindInstance, obj, tag)
	if err != nil {
		return ConfigInstance{}, err
	}
	var out ConfigInstance
	return out, json.Unmarshal(data, &out)
}

// UpdateInstance validates and stores a new revision of an existing instance.
func (s *Store) UpdateInstance(id string, inst ConfigInstance, tag string) (ConfigInstance, error) {
	set, err := s.setForInstance(inst)
	if err != nil {
		return ConfigInstance{}, fmt.Errorf("load set %q: %w", inst.SetID, err)
	}
	if err := inst.ValidateAgainst(set); err != nil {
		return ConfigInstance{}, err
	}
	if err := s.applyRules(&inst, set); err != nil {
		return ConfigInstance{}, err
	}
	obj, err := toMap(inst)
	if err != nil {
		return ConfigInstance{}, err
	}
	data, err := s.update(KindInstance, id, obj, tag)
	if err != nil {
		return ConfigInstance{}, err
	}
	var out ConfigInstance
	return out, json.Unmarshal(data, &out)
}

// SetForInstance is the exported loader used by apply/diff callers.
func (s *Store) SetForInstance(inst ConfigInstance) (ConfigSet, error) {
	return s.setForInstance(inst)
}

// ── rules ────────────────────────────────────────────────────────────────────

// GetRule returns the current revision of a CUE validation/transformation rule.
func (s *Store) GetRule(id string) (ConfigRule, error) {
	data, err := s.get(KindRule, id)
	if err != nil {
		return ConfigRule{}, err
	}
	var rule ConfigRule
	return rule, json.Unmarshal(data, &rule)
}

// GetRuleVersion returns a specific revision of a rule.
func (s *Store) GetRuleVersion(id string, version int) (ConfigRule, error) {
	data, err := s.getVersion(KindRule, id, version)
	if err != nil {
		return ConfigRule{}, err
	}
	var rule ConfigRule
	return rule, json.Unmarshal(data, &rule)
}

// CreateRule validates the rule's CUE source and stores it.
func (s *Store) CreateRule(rule ConfigRule, tag string) (ConfigRule, error) {
	if err := rule.Validate(); err != nil {
		return ConfigRule{}, err
	}
	obj, err := toMap(rule)
	if err != nil {
		return ConfigRule{}, err
	}
	_, data, err := s.create(KindRule, obj, tag)
	if err != nil {
		return ConfigRule{}, err
	}
	var out ConfigRule
	return out, json.Unmarshal(data, &out)
}

// UpdateRule validates and stores a new revision of an existing rule.
func (s *Store) UpdateRule(id string, rule ConfigRule, tag string) (ConfigRule, error) {
	if err := rule.Validate(); err != nil {
		return ConfigRule{}, err
	}
	obj, err := toMap(rule)
	if err != nil {
		return ConfigRule{}, err
	}
	data, err := s.update(KindRule, id, obj, tag)
	if err != nil {
		return ConfigRule{}, err
	}
	var out ConfigRule
	return out, json.Unmarshal(data, &out)
}

// rulesForSet loads every current-revision rule bound to the given set.
func (s *Store) rulesForSet(setID string) ([]ConfigRule, error) {
	metas, err := s.List(KindRule)
	if err != nil {
		return nil, err
	}
	var out []ConfigRule
	for _, m := range metas {
		if m.SetID != setID {
			continue
		}
		rule, err := s.GetRule(m.ID)
		if err != nil {
			continue
		}
		if !rule.IsEnabled() {
			continue
		}
		out = append(out, rule)
	}
	return out, nil
}

// applyRules runs every rule bound to inst's set against its values. On
// success it merges rule-derived values back into inst (parameter keys only) so
// the stored instance reflects the canonical, transformed configuration. A
// violation is returned as *RuleError so the caller can surface the structured
// details.
func (s *Store) applyRules(inst *ConfigInstance, set ConfigSet) error {
	rules, err := s.rulesForSet(inst.SetID)
	if err != nil {
		return err
	}
	if len(rules) == 0 {
		return nil
	}
	res := evaluateRules(rules, inst.Values)
	if !res.OK {
		return &RuleError{Result: res}
	}
	for k, v := range res.Transformed {
		if _, ok := set.param(k); ok {
			if inst.Values == nil {
				inst.Values = map[string]any{}
			}
			inst.Values[k] = v
		}
	}
	return nil
}

// ValidateInstanceRules evaluates the stored rules for an instance without
// persisting anything. It is the read-only counterpart used by the validate
// endpoint.
func (s *Store) ValidateInstanceRules(inst ConfigInstance) (RuleResult, error) {
	rules, err := s.rulesForSet(inst.SetID)
	if err != nil {
		return RuleResult{}, err
	}
	if len(rules) == 0 {
		return RuleResult{OK: true}, nil
	}
	return evaluateRules(rules, inst.Values), nil
}

// ── JSON helpers ────────────────────────────────────────────────────────────

func name(obj map[string]any) string {
	n, _ := obj["name"].(string)
	return n
}

func marshal(obj map[string]any) ([]byte, error) {
	return json.MarshalIndent(obj, "", "  ")
}

func unmarshal(data []byte) (map[string]any, error) {
	var obj map[string]any
	if err := json.Unmarshal(data, &obj); err != nil {
		return nil, err
	}
	return obj, nil
}

// toMap round-trips a typed value through JSON into a generic map so the store
// can stamp id/version/tag without knowing the concrete type.
func toMap(v any) (map[string]any, error) {
	data, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	return unmarshal(data)
}

// 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 "config"
	}
	return result
}
