package storage

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

// Drive INTENT model (slice 10 P3 self-heal). The agent persists, per external user-data drive
// (keyed by DURABLE-ID — UUID/WWN, never sdX/path), the operator/customer INTENT — which is distinct
// from "is it currently mounted". The self-heal reconciler acts ONLY on this intent, so an
// out-of-band unmount (a colleague's Proxmox action) — which records NO intent — gets healed, while an
// official eject is respected.
//
// Four states (3A):
//   - new            : durable-id NOT in the store. Never auto-mounted (the user must enroll it).
//   - enrolled       : desired = mounted + bound into the guest. Drift (present && unmounted) with
//                      this intent → reconcile.
//   - ejected        : an intentional, temporary unmount, set ONLY via the official eject endpoint.
//                      Present && unmounted → left alone. CLEARED to `enrolled` when the drive goes
//                      physically ABSENT, so a replug auto-mounts (the replug rule, for free).
//   - decommissioned : permanent. Never auto-mounted; SURVIVES absent/present; cleared only by an
//                      explicit re-commission.
//
// The elegant invariant: intent is recorded ONLY through the official enroll/eject/decommission
// paths. "Is it unmounted" ≠ "did someone officially eject it."
type DriveIntent string

const (
	IntentNew            DriveIntent = ""               // not in the store
	IntentEnrolled       DriveIntent = "enrolled"       // desired mounted → reconcile drift
	IntentEjected        DriveIntent = "ejected"        // intentional unmount → leave alone
	IntentDecommissioned DriveIntent = "decommissioned" // permanent → never auto-mount
)

// IntentStore is the durable, durable-id-keyed intent map. Thread-safe; atomic file writes
// (tmp+rename), 0600. Shared between the local-API (records enroll/eject) and the self-heal
// reconciler (reads to gate remounts; clears `ejected` on absent).
type IntentStore struct {
	path string
	mu   sync.Mutex
	m    map[string]DriveIntent // durable-id -> intent
}

// OpenIntentStore loads (or initializes) the store at path. A missing file is an empty store; a
// corrupt file is an error (fail loud — the reconciler's gate must not silently lose intent).
func OpenIntentStore(path string) (*IntentStore, error) {
	s := &IntentStore{path: path, m: map[string]DriveIntent{}}
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return s, nil
		}
		return nil, fmt.Errorf("intent store: read %s: %w", path, err)
	}
	if len(data) > 0 {
		if err := json.Unmarshal(data, &s.m); err != nil {
			return nil, fmt.Errorf("intent store: parse %s: %w", path, err)
		}
	}
	return s, nil
}

// Get returns the intent for a durable-id (IntentNew if absent). An empty durable-id is always
// IntentNew — the reconciler must never act on a drive whose identity it can't pin.
func (s *IntentStore) Get(durableID string) DriveIntent {
	if durableID == "" {
		return IntentNew
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.m[durableID]
}

// SetEnrolled records a drive as enrolled (on successful enroll/guest-attach). Idempotent.
func (s *IntentStore) SetEnrolled(durableID string) error { return s.set(durableID, IntentEnrolled) }

// SetEjected records an intentional eject (official eject endpoint only).
func (s *IntentStore) SetEjected(durableID string) error { return s.set(durableID, IntentEjected) }

// SetDecommissioned records a permanent decommission (operator path).
func (s *IntentStore) SetDecommissioned(durableID string) error {
	return s.set(durableID, IntentDecommissioned)
}

// OnAbsent transitions a drive that has gone physically ABSENT: an `ejected` drive becomes `enrolled`
// again (so a replug auto-mounts — the replug rule); `decommissioned` and `enrolled` are unchanged;
// `new` stays new. This is the ONLY place ejected→enrolled happens.
func (s *IntentStore) OnAbsent(durableID string) error {
	if durableID == "" {
		return nil
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.m[durableID] == IntentEjected {
		s.m[durableID] = IntentEnrolled
		return s.saveLocked()
	}
	return nil
}

func (s *IntentStore) set(durableID string, intent DriveIntent) error {
	if durableID == "" {
		return fmt.Errorf("intent store: refusing to record intent for an empty durable-id")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.m[durableID] == intent {
		return nil // idempotent — no write
	}
	s.m[durableID] = intent
	return s.saveLocked()
}

func (s *IntentStore) saveLocked() error {
	data, err := json.MarshalIndent(s.m, "", "  ")
	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)
}
