package localapi

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"sync"
)

// GuestBindStore persists, per guest, the DURABLE-IDs of the user-data drives enrolled (guest-attached)
// into it. F9: the in-guest bind (`pct set -mpN`) is config state that does NOT survive a destroy +
// re-provision, and nothing re-asserted it — so a re-provisioned guest came up with the HDD unattached
// even though it had been enrolled. This store is the record the startup re-assert (ReassertGuestBinds)
// replays: for each enrolled durable-id still physically present, re-add the bind if the guest lacks it.
//
// Keyed by durable-id (NOT host path or sdX) so the re-assert is "on durable-id match" — a swapped or
// absent drive is never auto-bound. Thread-safe; atomic file writes (tmp+rename), 0600. Mirrors
// storage.IntentStore.
type GuestBindStore struct {
	path string
	mu   sync.Mutex
	m    map[int][]string // vmid -> sorted set of enrolled durable-ids
}

// OpenGuestBindStore loads (or initializes) the store. Missing file = empty store; corrupt file = error
// (fail loud — losing a bind record would silently drop the re-assert).
func OpenGuestBindStore(path string) (*GuestBindStore, error) {
	s := &GuestBindStore{path: path, m: map[int][]string{}}
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return s, nil
		}
		return nil, fmt.Errorf("guest-bind store: read %s: %w", path, err)
	}
	if len(data) > 0 {
		// stored as {"<vmid>": ["durable-id", ...]} (string keys — JSON object keys are strings)
		raw := map[string][]string{}
		if err := json.Unmarshal(data, &raw); err != nil {
			return nil, fmt.Errorf("guest-bind store: parse %s: %w", path, err)
		}
		for k, v := range raw {
			vmid, err := strconv.Atoi(k)
			if err != nil {
				return nil, fmt.Errorf("guest-bind store: bad vmid key %q: %w", k, err)
			}
			s.m[vmid] = v
		}
	}
	return s, nil
}

// Record adds (vmid, durableID) to the enrolled set. Idempotent — no write if already present. A blank
// durable-id is refused (the re-assert must never act on a drive whose identity it can't pin).
func (s *GuestBindStore) Record(vmid int, durableID string) error {
	if durableID == "" {
		return fmt.Errorf("guest-bind store: refusing to record an empty durable-id for vmid %d", vmid)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, id := range s.m[vmid] {
		if id == durableID {
			return nil // idempotent
		}
	}
	s.m[vmid] = append(s.m[vmid], durableID)
	sort.Strings(s.m[vmid])
	return s.saveLocked()
}

// Remove drops (vmid, durableID) from the enrolled set. Idempotent — absent (vmid or id) is a no-op
// returning nil. Atomic write (tmp+rename) like Record/saveLocked. Called by the self-serve
// decommission endpoint so a permanently-removed drive no longer lingers in the startup re-assert
// record (hygiene — the intent-aware ReassertGuestBinds is the load-bearing guard).
func (s *GuestBindStore) Remove(vmid int, durableID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	ids, ok := s.m[vmid]
	if !ok {
		return nil
	}
	kept := ids[:0:0]
	found := false
	for _, id := range ids {
		if id == durableID {
			found = true
			continue
		}
		kept = append(kept, id)
	}
	if !found {
		return nil // idempotent: nothing to remove
	}
	if len(kept) == 0 {
		delete(s.m, vmid)
	} else {
		s.m[vmid] = kept
	}
	return s.saveLocked()
}

// Guests returns a copy of the vmid → enrolled-durable-ids map.
func (s *GuestBindStore) Guests() map[int][]string {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make(map[int][]string, len(s.m))
	for vmid, ids := range s.m {
		out[vmid] = append([]string(nil), ids...)
	}
	return out
}

func (s *GuestBindStore) saveLocked() error {
	raw := make(map[string][]string, len(s.m))
	for vmid, ids := range s.m {
		raw[strconv.Itoa(vmid)] = ids
	}
	data, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o600); err != nil {
		os.Remove(tmp)
		return err
	}
	return os.Rename(tmp, s.path)
}
