package conf

import (
	"sync"
	"sync/atomic"

	"google.golang.org/protobuf/proto"
)

// Runtime owns the active immutable configuration snapshot. Callers always
// receive clones so a config file reload cannot race with request handling.
type Runtime struct {
	snapshot  atomic.Pointer[runtimeSnapshot]
	mu        sync.RWMutex
	nextID    uint64
	listeners map[uint64]func(*Data, *AdminBackend)
}

type runtimeSnapshot struct {
	data  *Data
	admin *AdminBackend
}

func NewRuntime(data *Data, admin *AdminBackend) *Runtime {
	runtime := &Runtime{listeners: make(map[uint64]func(*Data, *AdminBackend))}
	runtime.Replace(data, admin)
	return runtime
}

func cloneData(value *Data) *Data {
	if value == nil {
		return nil
	}
	return proto.Clone(value).(*Data)
}

func cloneAdmin(value *AdminBackend) *AdminBackend {
	if value == nil {
		return nil
	}
	return proto.Clone(value).(*AdminBackend)
}

func (r *Runtime) Replace(data *Data, admin *AdminBackend) {
	data = cloneData(data)
	admin = cloneAdmin(admin)
	r.snapshot.Store(&runtimeSnapshot{data: data, admin: admin})
	r.mu.RLock()
	listeners := make([]func(*Data, *AdminBackend), 0, len(r.listeners))
	for _, listener := range r.listeners {
		listeners = append(listeners, listener)
	}
	r.mu.RUnlock()
	for _, listener := range listeners {
		listener(cloneData(data), cloneAdmin(admin))
	}
}

// Subscribe registers a callback invoked after every successful runtime
// configuration replacement. It is used by long-lived clients such as the
// logger and other long-lived clients that must follow runtime config changes.
func (r *Runtime) Subscribe(listener func(*Data, *AdminBackend)) func() {
	if listener == nil {
		return func() {}
	}
	r.mu.Lock()
	r.nextID++
	id := r.nextID
	r.listeners[id] = listener
	r.mu.Unlock()
	return func() {
		r.mu.Lock()
		delete(r.listeners, id)
		r.mu.Unlock()
	}
}

func (r *Runtime) Data() *Data {
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return nil
	}
	return cloneData(snapshot.data)
}

func (r *Runtime) Admin() *AdminBackend {
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return nil
	}
	return cloneAdmin(snapshot.admin)
}

func (r *Runtime) Values() (*Data, *AdminBackend) {
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return nil, nil
	}
	return cloneData(snapshot.data), cloneAdmin(snapshot.admin)
}

func (r *Runtime) ConfigPath() string {
	snapshot := r.snapshot.Load()
	if snapshot == nil || snapshot.admin == nil {
		return ""
	}
	return snapshot.admin.ConfigPath
}

func (r *Runtime) UpdateDatabase(database *Data_Database) {
	for {
		current := r.snapshot.Load()
		var data *Data
		var admin *AdminBackend
		if current != nil {
			data = cloneData(current.data)
			admin = cloneAdmin(current.admin)
		}
		if data == nil {
			data = &Data{}
		}
		data.Database = proto.Clone(database).(*Data_Database)
		next := &runtimeSnapshot{data: data, admin: admin}
		if r.snapshot.CompareAndSwap(current, next) {
			return
		}
	}
}
