package gorm

// TODO link godocs

import (
	
	
	
	
	
	
	
	
	
	
	

	_ 
	
	
	
	
	
	
	

	
	amhist 
	am 
)

type MatcherFn func(now *am.TimeIndex, query *gorm.DB) *gorm.DB

type Config struct {
	amhist.BaseConfig

	// amount of records to save in bulk (default: 1000)
	QueueBatch int32
	// amount of goroutines doing bulk saving (default: 10)
	SavePool int
}

// ///// ///// /////

// ///// SCHEMA

// ///// ///// /////
// TODO optimize: gen a dedicated schema for each machine?

// Machine is a SQL version of [amhist.MachineRecord].
type Machine struct {
	// PK

	ID uint32 `gorm:"primaryKey"`

	// rels

	Times  []Time
	States []State

	// data

	MachId     string `gorm:"column:mach_id;index:mach_id"`
	StateNames datatypes.JSON
	Schema     datatypes.JSON

	// human times

	// first time the machine has been tracked
	FirstTracking time.Time
	// last time a tracking of this machine has started
	LastTracking time.Time
	// last time a sync has been performed
	LastSync time.Time

	// data - machine times

	// current machine start tick
	MachTick uint32
	// current (total) machine time
	// TODO optimize: collect in a separate query
	MTime datatypes.JSON
	// sum of the current machine time
	MTimeSum uint64
	// next ID for time records
	NextId uint64

	// cache

	cacheMTime am.Time
}

// Time is a SQL version of [amhist.TimeRecord].
type Time struct {
	// PK

	ID        uint64 `gorm:"primaryKey;autoIncrement:false"`
	MachineID uint32 `gorm:"primaryKey"`

	// rels

	Ticks []Tick `gorm:"foreignKey:TimeID,MachineID;references:ID,MachineID"`

	// data

	// MutType is a mutation type.
	MutType am.MutationType
	// MTimeSum is a machine time sum after this transition.
	MTimeSum uint64
	// MTimeSum is a machine time sum after this transition for tracked states
	// only.
	MTimeTrackedSum uint64
	// MTimeDiffSum is a machine time difference for this transition.
	MTimeDiffSum uint64
	// MTimeDiffSum is a machine time difference for this transition for tracked
	// states only.
	MTimeTrackedDiffSum uint64
	// MTimeRecordDiffSum is a machine time difference since the previous
	// [amhist.TimeRecord].
	MTimeRecordDiffSum uint64
	// HTime is a human time in UTC.
	HTime time.Time
	// MTime is a machine time for tracked states after this mutation.
	MTimeTracked datatypes.JSON
	// MTimeTrackedDiff is a machine time diff compared to the previous mutation
	// (not a record). TODO make optional? can be generated
	MTimeTrackedDiff datatypes.JSON
	// TODO MTimeTrackedDiffCompact arpc-like clock encoding
	// MachTick is the machine tick at the time of this transition.
	MachTick uint32

	// cache

	cacheMTimeTracked am.Time

	// Transition

	TxId string `gorm:"index:tx_id"`

	// data

	TxSourceTx   *string
	TxSourceMach *string
	TxIsAuto     bool
	TxIsAccepted bool
	TxIsCheck    bool
	TxIsBroken   bool
	TxQueueLen   uint16

	// normal queue props

	TxQueuedAt   *uint64
	TxExecutedAt *uint64

	// extra

	TxCalled    datatypes.JSON
	TxArguments *datatypes.JSON
}

type State struct {
	// PK

	ID        uint   `gorm:"primaryKey"`
	MachineID string `gorm:"index:machine_state"`
	// Index is the state index in the machine (not the tracked states index).
	Index int `gorm:"index:machine_state"`

	// data

	Name string
}

type Tick struct {
	// PK

	TimeID    uint64 `gorm:"primaryKey;autoIncrement:false;index:activated"`
	MachineID uint32 `gorm:"primaryKey;autoIncrement:false;index:activated"`
	StateID   uint   `gorm:"primaryKey;autoIncrement:false;index:activated"`

	// rels

	State State

	// data

	Tick uint64
	// was the state activated in this transition?
	Activated bool `gorm:"index:activated"`
	// was the state deactivated in this transition?
	Deactivated bool
	// state is currently active
	Active bool
	// TODO last change distance
}

// ///// ///// /////

// ///// TRACER

// ///// ///// /////

type tracer struct {
	*am.TracerNoOp

	mem *Memory
}

func ( *tracer) ( am.Api) context.Context {
	 := .mem
	 := time.Now().UTC()
	var  error

	// locks
	.mx.Lock()
	defer .mx.Unlock()

	// select existing mach
	,  := GetMachine(.Db, .Id(), true)
	if  == nil {
		// initial default
		 = &Machine{
			MachId:        .Id(),
			FirstTracking: ,
			NextId:        1,
		}
	}

	// machine record
	 := .Time(nil)
	,  := json.Marshal()
	.MachId = .Mach.Id()
	.LastTracking = 
	.FirstTracking = 
	.LastSync = 
	.MTime = 
	.MTimeSum = .Sum(nil)
	.MachTick = .MachineTick()
	.cacheMTime = 
	.nextId.Store(.NextId)

	// schema
	if .Cfg.StoreSchema {
		// TODO cache
		.Schema,  = json.Marshal(.Schema())
		if  != nil {
			.onErr()
			return nil
		}
		.StateNames,  = json.Marshal(.StateNames())
		if  != nil {
			.onErr()
			return nil
		}
	}

	// states
	 := make(map[string]bool, len(.States))
	for ,  := range .States {
		.cacheDbIdxs[.Name] = .ID
		[.Name] = true
	}
	 := false
	for ,  := range .Cfg.TrackedStates {
		if [] {
			continue
		}

		// add state
		 = true
		.States = append(.States, State{
			MachineID: .Mach.Id(),
			Index:     .Mach.Index1(),
			Name:      ,
		})
	}

	// upsert machine
	 = .Db.Clauses(clause.OnConflict{
		Columns: []clause.Column{{Name: "id"}},
		DoUpdates: clause.AssignmentColumns([]string{
			"last_sync", "last_tracking", "mach_tick", "m_time", "m_time_sum",
			"schema"}),
		// TODO cascade delete data in other tables
	}).Create().Error
	if  != nil {
		.onErr()
		return nil
	}
	.machRec = 

	// get DB IDs for state names
	if  {
		, _ = GetMachine(.Db, .Id(), true)
		for ,  := range .States {
			.cacheDbIdxs[.Name] = .ID
		}
	}

	return nil
}

func ( *tracer) ( am.Api,  am.Schema) {
	 := .mem
	var  error

	// locks
	.mx.Lock()
	defer .mx.Unlock()

	// update states TODO move to Memory.UpdateTracked
	// db := gorm.G[State](m.Db)
	// for _, state := range m.BaseConfig.TrackedStates {
	// 	idx := m.Mach.Index1(state)
	// 	// skip existing
	// 	if slices.Contains(slices.Collect(maps.Keys(old)), state) {
	// 		continue
	// 	}
	//
	// 	// insert
	// 	err = db.Create(m.Mach.Ctx(), &State{
	// 		MachineID: machine.Id(),
	// 		Index:     idx,
	// 		Name:      state,
	// 	})
	// 	if err != nil {
	// 		m.onErr(err)
	// 		return
	// 	}
	// }

	// update schema
	if !.Cfg.StoreSchema {
		return
	}
	 := .machRec
	.Schema,  = json.Marshal(.Mach.Schema())
	if  != nil {
		.onErr()
		return
	}
	.StateNames,  = json.Marshal(.Mach.StateNames())
	if  != nil {
		.onErr()
		return
	}

	// sync mach record
	if  := .Db.Save(.machRec).Error;  != nil {
		.onErr(fmt.Errorf("failed to save: %w", ))
	}
}

func ( *tracer) ( *am.Transition) {
	// TODO SPLIT
	 := .mem
	if .Ctx.Err() != nil {
		_ = .Dispose()
		return
	}
	if (!.IsAccepted.Load() && !.Cfg.TrackRejected) || .Mutation.IsCheck {
		return
	}

	// locks
	.mx.Lock()
	defer .mx.Unlock()

	 := .Mach
	 := .CalledStates()
	 := .TimeAfter.DiffSince(.TimeBefore).
		ToIndex(.StateNames()).NonZeroStates()
	 := .Mutation
	 := .Cfg
	 := (.ChangedExclude || len(.Changed) == 0) &&
		(.CalledExclude || len(.Called) == 0)
	 := .TimeAfter
	 := .Filter(.cacheTrackedIdxs)
	 := .TimeBefore.Filter(.cacheTrackedIdxs)
	 := .Sum(nil)
	 := .Sum(nil)

	// process called
	for ,  := range .Called {
		 := slices.Contains(, )
		if  && .CalledExclude {
			 = false
			break
		} else if ! && !.CalledExclude {
			 = true
			break
		}
	}

	// process changed
	for ,  := range .Changed {
		 := slices.Contains(, )
		if  && .ChangedExclude {
			 = false
			break
		} else if ! && !.ChangedExclude {
			 = true
			break
		}
	}

	if ! {
		return
	}

	// json
	,  := json.Marshal(.Mutation.Called)
	 := .Mutation.MapArgs(.SemLogger().ArgsMapper())
	,  := json.Marshal()
	,  := json.Marshal()
	,  := json.Marshal()
	,  := json.Marshal(
		.DiffSince())

	// time record
	var  uint64
	if .mem.lastRec != nil {
		 =  - .mem.lastRec.MTimeSum
	}
	 := .MachineTick()
	 := time.Now().UTC()
	 := .nextId.Load()
	 := Time{
		ID:                  ,
		MachineID:           .machRec.ID,
		MutType:             .Type,
		MTimeSum:            ,
		MTimeTrackedSum:     ,
		MTimeDiffSum:         - .TimeBefore.Sum(nil),
		MTimeTrackedDiffSum:  - .TimeBefore.Sum(.cacheTrackedIdxs),
		MTimeRecordDiffSum:  ,
		HTime:               ,
		MTimeTracked:        ,
		MTimeTrackedDiff:    ,
		MachTick:            ,
		cacheMTimeTracked:   ,
	}

	// optional tx record
	if .StoreTransitions {
		.TxId = .Id
		.TxCalled = 
		.TxIsAuto = .IsAuto
		.TxIsAccepted = .IsAccepted.Load()
		.TxIsCheck = .IsCheck
		.TxIsBroken = .IsBroken.Load()
		.TxQueueLen = .QueueLen

		// optional fields
		if len() > 0 {
			 := datatypes.JSON()
			.TxArguments = &
		}
		if .Source != nil {
			.TxSourceMach = &.Source.MachId
			.TxSourceTx = &.Source.TxId
		}
		if .QueueTick > 0 {
			 := .QueueTick
			.TxExecutedAt = &
		}
	}

	// insert state ticks
	 := make([]Tick, len())
	 := 0
	for ,  := range .Cfg.TrackedStates {
		 := am.IsActiveTick([])
		 := Tick{
			StateID:   .cacheDbIdxs[],
			Tick:      [],
			TimeID:    ,
			MachineID: .machRec.ID,
			Active:    ,
		}

		// activated & deactivated
		if  &&
			(.lastRec == nil ||
				.lastRec.cacheMTimeTracked[] != []) {

			.Activated = true
		}
		if ! &&
			.lastRec != nil &&
			(.lastRec.cacheMTimeTracked[] != []) {

			.Deactivated = true
		}

		[] = 
		++
	}
	.queue.ticks = append(.queue.ticks, ...)

	// update machine record
	.machRec.MTime = 
	.machRec.MTimeSum = 
	.machRec.LastSync = 
	.machRec.MachTick = 
	.machRec.NextId =  + 1
	.machRec.cacheMTime = 
	.nextId.Store( + 1)

	// queue, cache, GC
	.queue.times = append(.queue.times, )
	.SavePending.Add(1)
	.lastRec = &
	// TODO ensure save after a delay
	if .SavePending.Load() >= .Cfg.QueueBatch {
		.syncMx.RLock()
		.writeDb(true)
		.checkGc()
	}
}

// ///// ///// /////

// ///// MEMORY

// ///// ///// /////

type queue struct {
	times []Time
	ticks []Tick
}

type Memory struct {
	*amhist.BaseMemory

	Db             *gorm.DB
	Cfg            *Config
	SavePending    atomic.Int32
	SaveInProgress atomic.Bool
	Saved          atomic.Uint64
	// Value of Saved at the end of the last GC
	SavedGc atomic.Uint64

	savePool *errgroup.Group
	// sync lock (read: flush, write: sync)
	syncMx sync.RWMutex
	// garbage collector lock (read: query, write: GC)
	gcMx sync.RWMutex
	// TODO use Ctx
	disposed atomic.Bool
	machRec  *Machine
	queue    *queue
	onErr    func(err error)
	// global lock, needed mostly for [Memory.MachineRecord].
	mx               sync.Mutex
	cacheTrackedIdxs []int
	cacheDbIdxs      map[string]uint
	tr               *tracer
	lastRec          *Time
	// TODO fix sqlite3: constraint failed:
	//  UNIQUE constraint failed: times.id, times.machine_id
	nextId           atomic.Uint64
}

func (
	 context.Context,  *gorm.DB,  am.Api,  Config,
	 func( error),
) (*Memory, error) {

	// update the DB schema
	 := .AutoMigrate(
		&Machine{}, &Time{}, &State{}, &Tick{},
	)
	if  != nil {
		return nil, 
	}

	// TODO view per machine with state names as columns
	// viewSQL := `
	// CREATE VIEW IF NOT EXISTS active_user_view(
	//    AlbumTitle,
	//    Minutes
	// ) AS
	// SELECT
	// 	id AS user_id,
	// 	username AS user_name,
	// 	email AS user_email
	// FROM
	// 	users
	// WHERE
	// 	is_active = 1;`
	//
	// if err := db.Exec(viewSQL).Error; err != nil {
	// 	log.Fatal("Failed to create view:", err)
	// }

	 := 
	if .MaxRecords <= 0 {
		.MaxRecords = 1000
	}
	if .QueueBatch <= 0 {
		.QueueBatch = 10
	}
	if .SavePool <= 0 {
		.SavePool = 10
	}

	// include allowlists in tracked states
	if !.CalledExclude {
		.TrackedStates = slices.Concat(.TrackedStates, .Called)
	}
	if !.ChangedExclude {
		.TrackedStates = slices.Concat(.TrackedStates, .Changed)
	}

	if .TrackedStates == nil {
		.TrackedStates = .StateNames()
	}

	.TrackedStates = .ParseStates(.TrackedStates)
	if len(.TrackedStates) == 0 {
		return nil, fmt.Errorf("%w: no states to track", am.ErrStateMissing)
	}

	// init and bind
	 := &Memory{
		Cfg:              &,
		Db:               ,
		savePool:         &errgroup.Group{},
		onErr:            ,
		queue:            &queue{},
		cacheTrackedIdxs: .Index(.TrackedStates),
		cacheDbIdxs:      make(map[string]uint),
	}
	.BaseMemory = amhist.NewBaseMemory(, , .BaseConfig, )
	.savePool.SetLimit(.SavePool)
	 := &tracer{
		mem: ,
	}
	.tr = 
	.MachineInit()
	.OnDispose(func( string,  context.Context) {
		 := .Dispose()
		if  != nil {
			.onErr()
		}
	})

	return , .BindTracer()
}

// FindLatest is [amhist.BaseMemory.FindLatest].
func ( *Memory) (
	 context.Context,  bool,  int,  amhist.Query,
) ([]*amhist.MemoryRecord, error) {

	if  := .ValidateQuery();  != nil {
		return nil, 
	}
	 := .Start
	 := .End

	return .Match(, , func( *am.TimeIndex,  *gorm.DB) *gorm.DB {

		// states conditions

		 := []string{}
		// Active
		for ,  := range .Active {
			 = .Where(+".active = ?", true)
			 = append(, )
		}
		// Activated
		for ,  := range .Activated {
			 = .Where(+".activated = ?", true)
			 = append(, )
		}
		// Inactive
		for ,  := range .Inactive {
			 = .Where(+".active = ?", false)
			 = append(, )
		}
		// Deactivated
		for ,  := range .Deactivated {
			 = .Where(+".deactivated = ?", true)
			 = append(, )
		}
		// MTimeStates
		for ,  := range .MTimeStates {
			 = .Where(+".m_time >= ?", .MTime[])
			 = append(, )
		}
		for ,  := range .MTimeStates {
			 = .Where(+".m_time <= ?", .MTime[])
			 = append(, )
		}

		// joins

		// states
		for ,  := range utils.SlicesUniq() {
			 = .JoinState(, )
		}
		// tx
		if  && .Cfg.StoreTransitions {
			 = .JoinTransition()
		}

		// time conditions

		// HTime
		if !.HTime.IsZero() && !.HTime.IsZero() {
			 = .Where(
				"times.h_time >= ? AND times.h_time <= ?",
				.HTime, .HTime,
			)
		}
		// MTimeSum
		if .MTimeSum != 0 && .MTimeSum != 0 {
			 = .Where(
				"times.m_time_sum >= ? AND times.m_time_sum <= ?",
				.MTimeSum, .MTimeSum,
			)
		}
		// MTimeTrackedSum
		if .MTimeTrackedSum != 0 && .MTimeTrackedSum != 0 {
			 = .Where(
				"times.m_time_tracked_sum >= ? AND times.m_time_tracked_sum <= ?",
				.MTimeTrackedSum, .MTimeTrackedSum,
			)
		}
		// MTimeDiff
		if .MTimeDiff != 0 && .MTimeDiff != 0 {
			 = .Where(
				"times.m_time_diff >= ? AND times.m_time_diff <= ?",
				.MTimeDiff, .MTimeDiff,
			)
		}
		// MTimeTrackedDiff
		if .MTimeTrackedDiff != 0 && .MTimeTrackedDiff != 0 {
			 = .Where(
				"times.m_time_tracked_diff >= ? AND times.m_time_tracked_diff <= ?",
				.MTimeTrackedDiff, .MTimeTrackedDiff,
			)
		}
		// MTimeRecordDiff
		if .MTimeRecordDiff != 0 && .MTimeRecordDiff != 0 {
			 = .Where(
				"times.m_time_record_diff >= ? AND times.m_time_record_diff <= ?",
				.MTimeRecordDiff, .MTimeRecordDiff,
			)
		}
		// MachTick
		if .MachTick != 0 && .MachTick != 0 {
			 = .Where(
				"times.mach_tick >= ? AND times.mach_tick <= ?",
				.MachTick, .MachTick,
			)
		}

		// order
		if len() > 0 {
			 = .Order([len()-1] + ".time_id DESC")
		} else {
			 = .Order("times.id DESC")
		}

		return 
	})
}

// MachineRecord is [amhist.BaseMemory.MachineRecord].
func ( *Memory) () *amhist.MachineRecord {
	.mx.Lock()
	defer .mx.Unlock()

	 := .machRec
	 := &amhist.MachineRecord{
		MachId:        .MachId,
		FirstTracking: .FirstTracking,
		LastTracking:  .LastTracking,
		LastSync:      .LastSync,
		MachTick:      .MachTick,
		MTime:         .cacheMTime,
		MTimeSum:      .MTimeSum,
		NextId:        .NextId,
	}

	if .Config().StoreSchema {
		 := json.Unmarshal(.Schema, &.Schema)
		if  != nil {
			.onErr()
			return nil
		}
		 = json.Unmarshal(.StateNames, &.StateNames)
		if  != nil {
			.onErr()
			return nil
		}
	}

	return 
}

// Dispose is [amhist.BaseMemory.Dispose]. TODO merge with ctx
func ( *Memory) () error {
	if !.disposed.CompareAndSwap(false, true) {
		return nil
	}
	.mx.Lock()
	defer .mx.Unlock()

	 := .Mach.DetachTracer(.tr)
	if ,  := .Db.DB();  != nil {
		return errors.Join(, )
	} else {
		return errors.Join(.Close(), )
	}
}

func ( *Memory) ( *gorm.DB,  string) *gorm.DB {
	// TODO lock, ok check
	 := .cacheDbIdxs[]

	return .Joins(utils.Sp(`
		JOIN ticks `++`
			ON `++`.time_id = times.id 
			AND `++`.machine_id = times.machine_id
	`)).Where(+".state_id = ?", )
}

func ( *Memory) ( *gorm.DB) *gorm.DB {
	// TODO test
	return .Preload("transitions")
}

// func (m *Memory) SelectTime(query *gorm.DB, name string) *gorm.DB {
// 	return m.Db.Debug().Model(&Time{})
// }

// Match returns the latest record that matches the given matcher function.
func ( *Memory) (
	 context.Context,  int,  MatcherFn,
) ([]*amhist.MemoryRecord, error) {
	// TODO validate state is tracked and err

	var  = []Time{}
	 := .Db.Model(&Time{})
	 := .Mach.Time(nil).ToIndex(.Mach.StateNames())
	 = (, )
	if  > 0 {
		 = .Limit()
	}
	 := .WithContext().
		// TODO optimize: dont select TX when not storing
		Find(&).Error
	// errs
	if .Err() != nil || .Ctx.Err() != nil {
		return nil, errors.Join(.Err(), .Ctx.Err())
	} else if  != nil {
		return nil, 
	} else if len() == 0 {
		return nil, nil
	}

	// build results
	var  = make([]*amhist.MemoryRecord, len())
	for ,  := range  {
		 := am.Time{}
		if  := json.Unmarshal(.MTimeTracked, &);  != nil {
			return nil, 
		}
		 := am.Time{}
		if  := json.Unmarshal(.MTimeTracked, &);  != nil {
			return nil, 
		}

		// time record
		[] = &amhist.MemoryRecord{
			Time: &amhist.TimeRecord{
				MutType:             .MutType,
				MTimeSum:            .MTimeSum,
				MTimeTrackedSum:     .MTimeTrackedSum,
				MTimeDiffSum:        .MTimeDiffSum,
				MTimeTrackedDiffSum: .MTimeTrackedDiffSum,
				MTimeRecordDiffSum:  .MTimeRecordDiffSum,
				MTimeTracked:        ,
				MTimeTrackedDiff:    ,
				HTime:               .HTime,
				MachTick:            .MachTick,
			},
		}

		// TODO extract

		// optional tx record
		if .Cfg.StoreTransitions {
			[].Transition = &amhist.TransitionRecord{
				TransitionId: .TxId,
				IsAuto:       .TxIsAuto,
				IsAccepted:   .TxIsAccepted,
				IsCheck:      .TxIsCheck,
				IsBroken:     .TxIsBroken,
				QueueLen:     .TxQueueLen,
				Called:       nil,
				Arguments:    nil,
			}

			// optional fields
			 := [].Transition
			if .TxSourceTx != nil && .TxSourceMach != nil {
				.SourceTx = *.TxSourceTx
				.SourceMach = *.TxSourceMach
			}
			if .TxQueuedAt != nil && .TxExecutedAt != nil {
				.QueuedAt = *.TxQueuedAt
				.ExecutedAt = *.TxExecutedAt
			}

			// json fields
			if .TxCalled != nil {
				if  := json.Unmarshal(.TxCalled, &.Called);  != nil {
					return nil, 
				}
			}
			if .TxArguments != nil {
				if  := json.Unmarshal(*.TxArguments, &.Arguments);  != nil {
					return nil, 
				}
			}
		}
	}

	return , nil
}

// func (m *Memory) dbTime() gorm.Interface[Time] {
// 	return gorm.G[Time](m.Db)
// }

func ( *Memory) () {
	// TODO flush WAL checkpoint?
	// maybe GC TODO cap to max diff
	 := .SavedGc.Load()
	 := .Saved.Load()
	if float32(-) <= float32(.Cfg.MaxRecords)*1.5 ||
		!.gcMx.TryLock() {

		return
	}

	 := gorm.G[Time](.Db)
	,  := .
		Where("machine_id = ?", .machRec.ID).
		Where("id NOT IN (?)", .
			Select("id").
			Where("machine_id = ?", .machRec.ID).
			Order("id DESC").
			Limit(.Cfg.MaxRecords)).
		Delete(.Ctx)
	if  != nil {
		.onErr(fmt.Errorf("failed to GC: %w", ))
	}

	.SavedGc.Store(.Saved.Load())
}

// Config is [amhist.BaseMemory.Config].
func ( *Memory) () amhist.BaseConfig {
	return .Cfg.BaseConfig
}

// Machine is [amhist.BaseMemory.Machine].
func ( *Memory) () am.Api {
	return .Mach
}

// Sync is [amhist.BaseMemory.Sync].
func ( *Memory) () error {
	.log("sync...")

	// locks
	.mx.Lock()
	defer .mx.Unlock()
	.syncMx.Lock()
	defer .syncMx.Unlock()
	.writeDb(false)

	.log("sync OK")

	return nil
}

// writeDb requires [Memory.mx].
func ( *Memory) ( bool) {
	if .SavePending.Load() <= 0 {
		return
	}

	 := .queue

	// copy
	 := *.machRec
	// TODO schema race?
	 := .times
	.times = nil
	 := .ticks
	.ticks = nil
	.log("writeDb for %d record", len())
	 := len()
	.SavePending.Add(-int32())

	// fork
	go .savePool.Go(func() error {
		if .disposed.Load() {
			return nil
		}
		if  {
			defer .syncMx.RUnlock()
		}

		// sync mach record TODO skip saving states
		if  := .Db.Save().Error;  != nil {
			.onErr(fmt.Errorf("failed to save: %w", ))
			return 
		}
		// TODO optimize: parallel save?
		// times
		 := gorm.G[Time](.Db)
		 := .CreateInBatches(.Mach.Ctx(), &, 100)
		if  != nil {
			.onErr()
			return 
		}

		// ticks
		 := gorm.G[Tick](.Db)
		 = .CreateInBatches(.Mach.Ctx(), &, 100)
		if  != nil {
			.onErr()
			return 
		}

		return nil
	})
}

func ( *Memory) ( string,  ...any) {
	if !.Cfg.Log {
		return
	}

	log.Printf(, ...)
}

// ///// ///// /////

// ///// DB

// ///// ///// /////

// NewDb returns a new SQLite DB for GORM.
func ( string,  bool) (*gorm.DB, *sql.DB, error) {
	// TODO logger
	if  == "" {
		 = "amhist"
	}

	// TODO log slow queries to onErr
	 := logger.Config{
		SlowThreshold:             time.Second,
		LogLevel:                  logger.Silent,
		Colorful:                  true,
		IgnoreRecordNotFoundError: true,
	}
	if  {
		.LogLevel = logger.Info
		.IgnoreRecordNotFoundError = false
	}

	// expose internal SQLite to share with other drivers
	,  := gorm.Open(gormlite.Open(+".sqlite"), &gorm.Config{
		Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), ),
	})
	if  != nil {
		return nil, nil, 
	} else if ,  := .DB();  != nil {
		return nil, nil, 
	} else {
		if !vfs.SupportsSharedMemory {
			if  = .Exec(`PRAGMA locking_mode=exclusive`).Error;  != nil {
				return nil, nil, 
			}
		}

		// enable WAL
		if  = .Exec(`PRAGMA journal_mode=wal;`).Error;  != nil {
			return nil, nil, 
		}

		return , , 
	}
}

// GetMachine returns a machine record for a given machine id.
func ( *gorm.DB,  string,  bool) (*Machine, error) {
	var  Machine
	 := .Where("mach_id = ?", )
	if  {
		 = .Preload("States")
	}
	// dont error on not found with Find
	if  := .First(&).Error;  != nil {

		return nil, 
	}

	return &, nil
}

// ListMachines returns a list of all machines in a database. TODO
func ( *gorm.DB) ([]*amhist.MachineRecord, error) {
	// TODO
	panic("not implemented")
}