package hook

import (
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"slices"
	"sync"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/opensvc/om3/v3/core/event"
	"github.com/opensvc/om3/v3/core/node"
	"github.com/opensvc/om3/v3/core/xconfig"
	"github.com/opensvc/om3/v3/daemon/msgbus"
	"github.com/opensvc/om3/v3/util/hostname"
	"github.com/opensvc/om3/v3/util/plog"
	"github.com/opensvc/om3/v3/util/pubsub"
	"github.com/opensvc/om3/v3/util/xmap"
)

type (
	Manager struct {
		// config is the node merged config
		config *xconfig.T

		ctx       context.Context
		cancel    context.CancelFunc
		log       *plog.Logger
		startedAt time.Time

		labelLocalhost pubsub.Label
		localhost      string

		sub   *pubsub.Subscription
		subQS pubsub.QueueSizer

		hooks map[string]hook

		wg sync.WaitGroup
	}

	hook struct {
		node.Hook
		cancel func()
	}
)

var (
	AllowedEvents = []string{
		"ArbitratorError",
		"EnterOverloadPeriod",
		"ForgetPeer",
		"HeartbeatAlive",
		"HeartbeatStale",
		"HeartbeatMessageTypeUpdated",
		"InstanceMonitorAction",
		"LeaveOverloadPeriod",
		"NodeAlive",
		"NodeFrozen",
		"NodeSplitAction",
		"NodeStale",
	}
)

func NewManager(drainDuration time.Duration, subQS pubsub.QueueSizer) *Manager {
	localhost := hostname.Hostname()
	return &Manager{
		log:       plog.NewDefaultLogger().Attr("pkg", "daemon/hook").WithPrefix("daemon: hook: "),
		localhost: localhost,

		labelLocalhost: pubsub.Label{"node", localhost},

		subQS: subQS,
		hooks: make(map[string]hook),
	}
}

func (t *Manager) Start(parent context.Context) error {
	t.log.Infof("starting")
	t.ctx, t.cancel = context.WithCancel(parent)
	initialNodeConfig := node.ConfigData.GetByNode(t.localhost)
	if initialNodeConfig == nil {
		return fmt.Errorf("node config not found for localhost: %s", t.localhost)
	}
	t.update(initialNodeConfig.Hooks)
	t.startSubscriptions()
	t.startUpdateLoop()
	t.log.Infof("started")
	return nil
}

func (t *Manager) startUpdateLoop() {
	go func() {
		for {
			select {
			case <-t.ctx.Done():
				return
			case i := <-t.sub.C:
				switch ev := i.(type) {
				case *msgbus.NodeConfigUpdated:
					t.update(ev.Value.Hooks)
				}
			}
		}
	}()
}

func (t *Manager) startSubscriptions() {
	sub := pubsub.SubFromContext(t.ctx, "daemon.hook", t.subQS)
	sub.AddFilter(&msgbus.NodeConfigUpdated{}, t.labelLocalhost)
	sub.Start()
	t.sub = sub
}

func (t *Manager) Stop() error {
	t.log.Infof("stopping")
	defer t.log.Infof("stopped")
	t.cancel()
	t.wg.Wait()
	return nil
}

func (t *Manager) update(hooks []node.Hook) {
	currentHookNames := xmap.Keys(t.hooks)
	var hooksToStop, scannedHooks []string
	hooksToStart := make(map[string]node.Hook)
	for _, h := range hooks {
		name := h.Name
		scannedHooks = append(scannedHooks, name)
		current, ok := t.hooks[name]
		if !ok {
			hooksToStart[name] = h
			continue
		}
		if diff := current.Hook.Diff(h); diff != "" {
			t.log.Infof("%s: %s", name, diff)
			hooksToStop = append(hooksToStop, name)
			hooksToStart[name] = h
		}

	}
	for _, name := range currentHookNames {
		if !slices.Contains(scannedHooks, name) {
			hooksToStop = append(hooksToStop, name)
		}
	}
	for _, name := range hooksToStop {
		if t.hooks[name].cancel != nil {
			t.hooks[name].cancel()
		}
		delete(t.hooks, name)
	}
	for name, hookToStart := range hooksToStart {
		kinds := hookToStart.Events
		h := hook{
			Hook: hookToStart,
		}
		if len(hookToStart.Command) < 1 {
			t.log.Warnf("%s: empty command", name)
			continue
		}
		h.cancel = t.startHook(name, kinds, hookToStart.Command)
		t.hooks[name] = h
	}
}

func (t *Manager) hookExec(ctx context.Context, ev *event.Event, args []string) error {
	b, err := json.Marshal(ev)
	if err != nil {
		return fmt.Errorf("failed to json-encode event: %w", err)
	}
	cmd := exec.CommandContext(ctx, args[0], args[1:]...)
	cmd.Env = []string{
		"EVENT=" + string(b),
	}
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true, // Create a new process group
	}
	if err := cmd.Start(); err != nil {
		return fmt.Errorf("failed to start cmd: %w", err)
	}
	return cmd.Wait()
}

func (t *Manager) hookLoop(ctx context.Context, sub *pubsub.Subscription, name string, kinds, args []string) {
	t.log.Infof("%s: listening for events %s", name, kinds)
	defer t.log.Infof("%s: stop listening for events %s", name, kinds)
	var (
		running atomic.Bool
		j       uint64
	)

	for {
		select {
		case <-ctx.Done():
			return
		case i := <-sub.C:
			switch c := i.(type) {
			case *msgbus.AuditStart:
				t.log.HandleAuditStart(c.Q, c.Subsystems, "hook")
			case *msgbus.AuditStop:
				t.log.HandleAuditStop(c.Q, c.Subsystems, "hook")
			}
			j += 1
			ev := event.ToEvent(i, j)
			if ev == nil {
				continue
			}
			if running.Load() {
				t.log.Warnf("%s: on %s command is too slow => skip exec", name, ev.Kind)
				continue
			}
			running.Store(true)
			go func() {
				defer running.Store(false)
				t.log.Infof("%s: on %s exec %s", name, ev.Kind, args)
				if err := t.hookExec(ctx, ev, args); err != nil {
					t.log.Warnf("%s: %s", name, err)
				}
			}()
		}
	}
}

func (t *Manager) startHook(name string, kinds []string, args []string) func() {
	ctx, cancel := context.WithCancel(t.ctx)
	sub := pubsub.SubFromContext(ctx, "daemon.hook", t.subQS, pubsub.Timeout(time.Second))
	sub.AddFilter(&msgbus.AuditStart{})
	sub.AddFilter(&msgbus.AuditStop{})
	added := 0
	for _, kind := range kinds {
		if !slices.Contains(AllowedEvents, kind) {
			t.log.Warnf("%s: event %s is not allowed in hooks", name, kind)
			continue
		}
		event, err := msgbus.KindToT(kind)
		if err != nil {
			t.log.Warnf("%s: invalid event %s: %s", name, kind, err)
			continue
		}
		sub.AddFilter(event)
		added += 1
	}
	if added == 0 {
		cancel()
		return nil
	}
	sub.Start()

	go func() {
		t.hookLoop(ctx, sub, name, kinds, args)
		sub.Stop()
	}()
	return cancel
}
