// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket.
package kobject

import (
	"context"
	"fmt"
	"maps"
	"slices"
	"strconv"
	"sync"

	"hakurei.app/internal/report"
	"hakurei.app/internal/uevent"
)

const (
	// StateColdboot denotes an [Object] populated by a coldboot event. It is
	// eligible for all event actions.
	StateColdboot = iota
	// StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD]
	// event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or
	// has been targeted by a [uevent.KOBJ_UNBIND] event.
	StateNew
	// StateBound denotes an [Object] that has been targeted by a
	// [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND]
	// after that.
	StateBound
)

// Object represents a kernel object.
type Object struct {
	// Origin of the object.
	State int `json:"state,omitempty"`
	// Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE].
	Offline bool `json:"offline,omitempty"`

	// alloc_uevent_skb: devpath
	DevPath string `json:"devpath"`
	// registered per-driver (optional)
	ModAlias string `json:"modalias,omitempty"`
	// dev_driver_uevent: drv->name (optional)
	Driver string `json:"driver,omitempty"`

	// SUBSYSTEM value set by the kernel.
	Subsystem string `json:"subsystem"`

	// Uninterpreted environment variable pairs. An entry missing a separator
	// gains the value "\x00".
	Env map[string]string `json:"env"`
}

// Clone returns the address of a copy of o.
func (o *Object) Clone() *Object {
	v := *o
	v.Env = maps.Clone(o.Env)
	return &v
}

// GoString returns compound literal for the underlying value.
func (o *Object) GoString() string {
	return fmt.Sprintf("&%#v", *o)
}

// merge merges uninterpreted environment variable pairs from an [Event].
func (o *Object) merge(env map[string]string) {
	for k, v := range env {
		if v == "\x00" {
			continue
		}

		switch k {
		case "MODALIAS":
			o.ModAlias = v
			continue

		case "DRIVER":
			o.Driver = v
			continue

		default:
			if o.Env == nil {
				o.Env = make(map[string]string)
			}
			o.Env[k] = v
		}
	}
}

// update updates o with pairs from env, optionally stripping visited pairs.
func (o *Object) update(env map[string]string, strip bool) {
	for k := range o.Env {
		if v, ok := env[k]; ok {
			if strip {
				delete(env, k)
			}
			o.Env[k] = v
		}
	}
}

// A pendingIterator is a callback currently iterating through objects targeted
// by ongoing events.
type pendingIterator struct {
	f    func(o *Object, act uevent.KobjectAction) bool
	done chan<- struct{}
}

// State processes a stream of [Event] populated from [uevent.Message] received
// from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation
// of kernel state.
type State struct {
	// Next expected SEQNUM.
	seq uint64
	// DevPath to environment variables.
	uevent map[string]*Object
	// Synchronises access to uevent and its objects.
	ueventMu sync.RWMutex
	// Alive iterators.
	iter []*pendingIterator
	// Synchronises access to iter.
	iterMu sync.Mutex
	// UUID for synthetic [uevent.Coldboot] events.
	coldboot uevent.UUID
	// Called on [uevent.KOBJ_CHANGE] with stripped environment variables.
	handleChange func(o *Object, env map[string]string)
	// Reports errors populating [Event] from [uevent.Message]. A user-supplied
	// nil value is replaced with a noop.
	reportErr func(error)
}

// New returns the address of a new [State].
func New(
	coldboot uevent.UUID,
	handleChange func(o *Object, env map[string]string),
	reportErr func(error),
) *State {
	return &State{
		uevent:       make(map[string]*Object),
		coldboot:     coldboot,
		handleChange: handleChange,
		reportErr:    reportErr,
	}
}

// deleteIter removes an iterator from s. Must be called after acquiring iterMu.
func (s *State) deleteIter(p *pendingIterator) {
	s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool {
		return p == v
	})
}

// dispatchIter broadcasts an [Object] to all alive iterators.
func (s *State) dispatchIter(o *Object, act uevent.KobjectAction) {
	s.iterMu.Lock()
	defer s.iterMu.Unlock()

	for _, p := range s.iter {
		if !p.f(o, act) {
			s.deleteIter(p)
			close(p.done)
		}
	}
}

// Range calls f on all current and upcoming [Object] values tracked by s until
// f returns false or the context is cancelled. f must not retain o or modify
// the value it points to.
func (s *State) Range(
	ctx context.Context,
	f func(o *Object, act uevent.KobjectAction) bool,
) {
	done := make(chan struct{})
	p := pendingIterator{f, done}

	s.iterMu.Lock()
	s.ueventMu.RLock()
	for _, o := range s.uevent {
		if !f(o, uevent.KOBJ_ADD) {
			s.ueventMu.RUnlock()
			s.iterMu.Unlock()
			return
		}
	}
	s.ueventMu.RUnlock()
	s.iter = append(s.iter, &p)
	s.iterMu.Unlock()

	select {
	case <-ctx.Done():
		s.iterMu.Lock()
		s.deleteIter(&p)
		s.iterMu.Unlock()
		return

	case <-done:
		// deregistered by dispatchIter
		return
	}
}

// An EventError describes a malformed or inconsistent [Event].
type EventError struct {
	Kind int     `json:"fault"`
	E    Event   `json:"event"`
	O    *Object `json:"object,omitempty"`
}

var _ report.RepresentableError = EventError{}

func (EventError) Representable() {}

const (
	// EUnexpectedColdboot is reported for a coldboot event with action other
	// than the expected [uevent.KOBJ_ADD].
	EUnexpectedColdboot = iota
	// EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a
	// still-existing entry that was not the result of a coldboot.
	EDuplicateAdd
	// EBadTarget is reported for an event on a nonexistent [Object]. This is
	// generally only possible before coldboot completes.
	EBadTarget
	// ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an
	// entry in a state other than [StateColdboot] and [StateNew].
	ERemoveState
	// EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or
	// [uevent.KOBJ_ONLINE] event targeting an already offline or online object.
	EUnexpectedOffline
	// EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry
	// in a state other than [StateColdboot] and [StateNew].
	EBindState
	// EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an
	// entry in a state other than [StateBound].
	EUnbindState
	// EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the
	// DEVPATH_OLD environment variable.
	EMalformedMove
)

func (e EventError) Error() string {
	switch e.Kind {
	case EUnexpectedColdboot:
		return "unexpected " + e.E.Action.String() + " coldboot event"
	case EDuplicateAdd:
		return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath)
	case EBadTarget:
		return "unexpected " + e.E.Action.String() + " event on devpath " +
			strconv.Quote(e.E.DevPath)
	case ERemoveState:
		if e.O == nil {
			return "invalid remove event error"
		}
		return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) +
			" in state " + strconv.Itoa(e.O.State)
	case EUnexpectedOffline:
		if e.O == nil {
			return "invalid unexpected offline error"
		}
		if e.O.Offline {
			return "offline event targeting devpath " + strconv.Quote(e.E.DevPath)
		}
		return "online event targeting devpath " + strconv.Quote(e.E.DevPath)
	case EBindState:
		if e.O == nil {
			return "invalid bind state error"
		}
		return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) +
			" in state " + strconv.Itoa(e.O.State)
	case EUnbindState:
		if e.O == nil {
			return "invalid unbind state error"
		}
		return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) +
			" in state " + strconv.Itoa(e.O.State)
	case EMalformedMove:
		return "move event targeting devpath " + strconv.Quote(e.E.DevPath) +
			" missing DEVPATH_OLD"

	default:
		return "invalid event error kind " + strconv.Itoa(e.Kind)
	}
}

// NewError returns a new [EventError] for e and o.
func (e *Event) NewError(kind int, o *Object) error {
	if o != nil {
		o = o.Clone()
	}
	return EventError{kind, e.Clone(), o}
}

// processEvent merges an event into s.
func (s *State) processEvent(e *Event) {
	s.ueventMu.Lock()
	defer s.ueventMu.Unlock()

	coldboot := e.Synth != nil
	if e.Action != uevent.KOBJ_ADD && coldboot {
		s.reportErr(e.NewError(EUnexpectedColdboot, nil))
		return
	}

	switch act := e.Action; act {
	case uevent.KOBJ_ADD:
		if e.Synth == nil {
			if o, ok := s.uevent[e.DevPath]; ok {
				s.reportErr(e.NewError(EDuplicateAdd, o))
				o.merge(e.Env)
				s.dispatchIter(o, act)
				return
			}
		}
		o := e.makeColdboot()
		if !coldboot {
			o.State = StateNew
		}
		o.merge(e.Env)
		s.uevent[e.DevPath] = o
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_REMOVE:
		if o, ok := s.uevent[e.DevPath]; !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			return
		} else if o.State != StateColdboot && o.State != StateNew {
			s.reportErr(e.NewError(ERemoveState, o))
		}
		delete(s.uevent, e.DevPath)
		return

	case uevent.KOBJ_CHANGE:
		o, ok := s.uevent[e.DevPath]
		if !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// this suffers from the coldboot race window similar to KOBJ_MOVE,
			// however this action combines driver-specific and change-specific
			// environment variables and combines them with environment
			// variables meant to convey state of the kobject, and it is not
			// possible to reliably separate them, so this fallback avoids the
			// race at the cost of including some garbage in tracked state
			o = e.makeColdboot()
			o.merge(e.Env)
			s.uevent[e.DevPath] = o
			s.dispatchIter(o, act)
			return
		}
		o.update(e.Env, true)
		if s.handleChange != nil {
			s.handleChange(o, e.Env)
		}
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_MOVE:
		var o *Object
		if old, ok := e.Env["DEVPATH_OLD"]; !ok {
			s.reportErr(e.NewError(EMalformedMove, nil))
			// not reached
			o = e.makeColdboot()
		} else if o, ok = s.uevent[old]; !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// this generally happens during coldboot, dropping the event here
			// may cause inconsistent state if the coldboot event for this
			// object was generated before the bind event
			delete(e.Env, "DEVPATH_OLD")
			o = e.makeColdboot()
		} else {
			delete(s.uevent, old)
			delete(e.Env, "DEVPATH_OLD")
		}
		o.merge(e.Env)
		s.uevent[e.DevPath] = o
		o.DevPath = e.DevPath
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_ONLINE:
		o, ok := s.uevent[e.DevPath]
		if !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// coldboot race window similar to an unexpected KOBJ_MOVE
			o = e.makeColdboot()
			s.uevent[e.DevPath] = o
			o.merge(e.Env)
		}
		if !o.Offline {
			s.reportErr(e.NewError(EUnexpectedOffline, o))
		}
		o.Offline = false
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_OFFLINE:
		o, ok := s.uevent[e.DevPath]
		if !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// coldboot race window similar to an unexpected KOBJ_MOVE
			o = e.makeColdboot()
			s.uevent[e.DevPath] = o
			o.merge(e.Env)
		}
		if o.Offline {
			s.reportErr(e.NewError(EUnexpectedOffline, o))
		}
		o.Offline = true
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_BIND:
		o, ok := s.uevent[e.DevPath]
		if !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// coldboot race window similar to an unexpected KOBJ_MOVE
			o = e.makeColdboot()
			s.uevent[e.DevPath] = o
		}
		if o.State != StateColdboot && o.State != StateNew {
			s.reportErr(e.NewError(EBindState, o))
		}
		o.State = StateBound
		o.merge(e.Env)
		s.dispatchIter(o, act)
		return

	case uevent.KOBJ_UNBIND:
		o, ok := s.uevent[e.DevPath]
		if !ok {
			s.reportErr(e.NewError(EBadTarget, nil))
			// coldboot race window similar to an unexpected KOBJ_MOVE, but does
			// not result in inconsistent state if dropped
			return
		}
		if o.State != StateBound {
			s.reportErr(e.NewError(EUnbindState, o))
		}
		o.State = StateNew
		o.Driver = ""
		s.dispatchIter(o, act)
		return

	default: // not reached
		s.reportErr(fmt.Errorf("invalid action %d", e.Action))
		return
	}
}

// BadSequenceError is reported for an unexpected SEQNUM.
type BadSequenceError struct{ Got, Want uint64 }

func (e BadSequenceError) Error() string {
	return "SEQNUM=" + strconv.FormatUint(e.Got, 10) +
		", want " + strconv.FormatUint(e.Want, 10)
}

// Consume receives uevent messages and updates s to reflect state of kernel.
func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) {
	if s.uevent == nil {
		s.uevent = make(map[string]*Object)
	}
	if s.reportErr == nil {
		s.reportErr = func(error) {}
	}

	var e Event
	for {
		select {
		case <-ctx.Done():
			return

		case m, ok := <-events:
			if !ok {
				return
			}
			e.Populate(s.reportErr, m)

			// skip external synthetic event
			if e.Synth != nil && *e.Synth != s.coldboot {
				continue
			}

			if s.seq == 0 {
				s.seq = e.Sequence
			}
			if s.seq != e.Sequence {
				s.reportErr(BadSequenceError{e.Sequence, s.seq})
			}
			s.seq++
			s.processEvent(&e)
		}
	}
}
