package plugins

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"sync"

	"drivervault/apiserver/internal/pb"
)

// Store is where a Manager keeps plugin enable-state and global (L1) config.
//
// It deals in the raw JSON document rather than in records, so the Manager does
// not care where that document lives: PocketBase in production, memory in tests.
type Store interface {
	// Load returns the stored document. found is false when the store holds no
	// document yet — a fresh install, nothing configured.
	//
	// A returned error means the store could not be reached or read. It must
	// NOT be taken as "no settings": the Manager stays unloaded and refuses to
	// save, so a transient outage cannot overwrite settings it never read.
	Load(ctx context.Context) (data []byte, found bool, err error)

	// Save replaces the stored document.
	Save(ctx context.Context, data []byte) error

	// Describe names the store for log and error messages.
	Describe() string
}

// PluginSettingsField is the record field the document lives in. The org (L2)
// and user (L3) layers of the cascade keep their plugin config in a field of
// this name too (see internal/api/integrations.go), so the global layer is
// stored the same way they are rather than being a special case.
//
// The documents are not identical in shape: L2/L3 hold per-plugin user config
// only, while L1 additionally carries the enable state and the registration of
// external plugins. Same mechanism and same field, different payload.
const PluginSettingsField = "pluginSettings"

// globalSettingsKey identifies the singleton record holding the global layer.
// It is a fixed constant, never caller input.
const globalSettingsKey = "global"

// --- PocketBase-backed store (production) -----------------------------------

// pbStore keeps the document in a single record of the app_settings collection,
// identified by key="global". This is the only store the server itself uses:
// nothing about the plugin layer touches the filesystem.
type pbStore struct {
	client     *pb.Client
	collection string

	mu sync.Mutex
	id string // cached id of the singleton record, resolved on first Load/Save
}

// NewPocketBaseStore returns a Store backed by the singleton settings record in
// the given collection.
func NewPocketBaseStore(client *pb.Client, collection string) Store {
	return &pbStore{client: client, collection: collection}
}

func (s *pbStore) Describe() string {
	return "PocketBase " + s.collection + " (key=" + globalSettingsKey + ")"
}

// settingsRecord is the slice of the singleton record this store reads.
type settingsRecord struct {
	ID             string          `json:"id"`
	PluginSettings json.RawMessage `json:"pluginSettings"`
}

func (s *pbStore) Load(ctx context.Context) ([]byte, bool, error) {
	if !s.client.Configured() {
		return nil, false, fmt.Errorf("%w: no PocketBase service account configured", errNotReady)
	}
	rec, found, err := s.find(ctx)
	if err != nil {
		return nil, false, err
	}
	if !found {
		return nil, false, nil
	}
	s.mu.Lock()
	s.id = rec.ID
	s.mu.Unlock()
	return rec.PluginSettings, true, nil
}

// find resolves the singleton record. A missing record is not an error.
func (s *pbStore) find(ctx context.Context) (settingsRecord, bool, error) {
	q := url.Values{}
	q.Set("filter", "key='"+globalSettingsKey+"'")
	q.Set("perPage", "1")

	res, err := s.client.List(ctx, s.collection, q)
	if err != nil {
		if isNotFound(err) {
			// A 404 from a list means the collection itself is absent: listing an
			// existing but empty collection answers 200 with no items. Tagged
			// apart from a plain outage because retrying a read against a
			// collection that does not exist can never succeed — the caller has
			// to create it. Still a flavour of not-ready, so nothing is
			// overwritten in the meantime.
			return settingsRecord{}, false, fmt.Errorf("%w: %v", errNoCollection, err)
		}
		return settingsRecord{}, false, fmt.Errorf("%w: %v", errNotReady, err)
	}
	var items []settingsRecord
	if err := json.Unmarshal(res.Items, &items); err != nil {
		return settingsRecord{}, false, fmt.Errorf("%w: %v", errNotReady, err)
	}
	if len(items) == 0 {
		return settingsRecord{}, false, nil
	}
	return items[0], true, nil
}

func (s *pbStore) Save(ctx context.Context, data []byte) error {
	if !s.client.Configured() {
		return fmt.Errorf("%w: no PocketBase service account configured", errPersist)
	}

	s.mu.Lock()
	id := s.id
	s.mu.Unlock()

	// Resolve the record id if this process has not seen it yet (or if the
	// record was recreated behind our back).
	if id == "" {
		rec, found, err := s.find(ctx)
		if err != nil {
			return fmt.Errorf("%w: %v", errPersist, err)
		}
		if found {
			id = rec.ID
		}
	}

	payload := map[string]any{
		"key":               globalSettingsKey,
		PluginSettingsField: json.RawMessage(data),
	}

	if id != "" {
		if err := s.client.Update(ctx, s.collection, id, payload, nil); err != nil {
			if !isNotFound(err) {
				return fmt.Errorf("%w: %v", errPersist, err)
			}
			// The record was deleted since we resolved it; fall through and
			// recreate it rather than losing the save.
			s.mu.Lock()
			s.id = ""
			s.mu.Unlock()
			id = ""
		} else {
			s.mu.Lock()
			s.id = id
			s.mu.Unlock()
			return nil
		}
	}

	var created settingsRecord
	if err := s.client.Create(ctx, s.collection, payload, &created); err != nil {
		return fmt.Errorf("%w: %v", errPersist, err)
	}
	s.mu.Lock()
	s.id = created.ID
	s.mu.Unlock()
	return nil
}

// isNotFound reports whether err is a PocketBase 404.
func isNotFound(err error) bool {
	var apiErr *pb.APIError
	return errors.As(err, &apiErr) && apiErr.Status == 404
}

// --- In-memory store (tests) ------------------------------------------------

// memoryStore holds the document in memory. failLoad/failSave let a test drive
// the not-ready and failed-save paths.
type memoryStore struct {
	mu       sync.Mutex
	data     []byte
	found    bool
	failLoad error
	failSave error
}

// NewMemoryStore returns a Store that keeps the document in memory, seeded with
// document (nil for an empty store). Intended for tests.
func NewMemoryStore(document []byte) Store {
	return &memoryStore{data: document, found: document != nil}
}

func (s *memoryStore) Describe() string { return "memory" }

func (s *memoryStore) Load(ctx context.Context) ([]byte, bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.failLoad != nil {
		return nil, false, s.failLoad
	}
	return s.data, s.found, nil
}

func (s *memoryStore) Save(ctx context.Context, data []byte) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.failSave != nil {
		return s.failSave
	}
	s.data = append([]byte(nil), data...)
	s.found = true
	return nil
}
