package orm

import "fmt"

// ComputeFunc is a function that computes field values for a recordset.
// Mirrors: @api.depends decorated methods in Odoo.
//
// The function receives a singleton recordset and must return a Values map
// with the computed field values.
//
// Example:
//
//	func computeAmount(rs *Recordset) (Values, error) {
//	    total := 0.0
//	    // ... sum line amounts ...
//	    return Values{"amount_total": total}, nil
//	}
type ComputeFunc func(rs *Recordset) (Values, error)

// RegisterCompute registers a compute function for a field.
// The same function can compute multiple fields (call RegisterCompute for each).
func (m *Model) RegisterCompute(fieldName string, fn ComputeFunc) {
	if m.computes == nil {
		m.computes = make(map[string]ComputeFunc)
	}
	m.computes[fieldName] = fn
}

// SetupComputes builds the reverse dependency map for this model.
// Called after all modules are loaded.
//
// For each field with Depends, creates a mapping:
//
//	trigger_field → []computed_field_names
//
// So when trigger_field is written, we know which computes to re-run.
func (m *Model) SetupComputes() {
	m.dependencyMap = make(map[string][]string)
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if f.Compute == "" || len(f.Depends) == 0 {
			continue
		}
		for _, dep := range f.Depends {
			m.dependencyMap[dep] = append(m.dependencyMap[dep], name)
		}
	}
}

// RunStoredComputes runs compute functions for stored computed fields
// and merges results into vals. Called before INSERT in Create().
func RunStoredComputes(m *Model, env *Environment, id int64, vals Values) error {
	if len(m.computes) == 0 {
		return nil
	}

	// Collect stored computed fields that have registered functions
	seen := make(map[string]bool) // Track compute functions already called (by field name)

	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if f.Compute == "" || !f.Store {
			continue
		}
		fn, ok := m.computes[name]
		if !ok {
			continue
		}
		// Deduplicate: same function may compute multiple fields
		if seen[f.Compute] {
			continue
		}
		seen[f.Compute] = true

		// Create a temporary recordset for the computation
		rs := &Recordset{env: env, model: m, ids: []int64{id}}
		computed, err := fn(rs)
		if err != nil {
			return fmt.Errorf("orm: compute %s.%s: %w", m.name, name, err)
		}

		// Merge computed values
		for k, v := range computed {
			cf := m.GetField(k)
			if cf != nil && cf.Store && cf.Compute != "" {
				vals[k] = v
			}
		}
	}
	return nil
}

// TriggerRecompute re-computes stored fields that depend on written fields.
// Called after UPDATE in Write().
func TriggerRecompute(rs *Recordset, writtenFields Values) error {
	m := rs.model
	if len(m.dependencyMap) == 0 || len(m.computes) == 0 {
		return nil
	}

	// Find which computed fields need re-computation
	toRecompute := make(map[string]bool)
	for fieldName := range writtenFields {
		for _, computedField := range m.dependencyMap[fieldName] {
			toRecompute[computedField] = true
		}
	}

	if len(toRecompute) == 0 {
		return nil
	}

	// Run computes for each record
	seen := make(map[string]bool)
	for _, id := range rs.IDs() {
		singleton := rs.Browse(id)
		recomputedVals := make(Values)

		for fieldName := range toRecompute {
			f := m.GetField(fieldName)
			if f == nil || !f.Store {
				continue
			}
			fn, ok := m.computes[fieldName]
			if !ok {
				continue
			}
			if seen[f.Compute] {
				continue
			}
			seen[f.Compute] = true

			computed, err := fn(singleton)
			if err != nil {
				return fmt.Errorf("orm: recompute %s.%s: %w", m.name, fieldName, err)
			}
			for k, v := range computed {
				cf := m.GetField(k)
				if cf != nil && cf.Store && cf.Compute != "" {
					recomputedVals[k] = v
				}
			}
		}

		// Write recomputed values directly to DB (bypass hooks to avoid infinite loop)
		if len(recomputedVals) > 0 {
			if err := writeDirectNohook(rs.env, m, id, recomputedVals); err != nil {
				return err
			}
		}

		// Reset seen for next record
		seen = make(map[string]bool)
	}
	return nil
}

// writeDirectNohook writes values directly without triggering hooks or recomputes.
func writeDirectNohook(env *Environment, m *Model, id int64, vals Values) error {
	var setClauses []string
	var args []interface{}
	idx := 1

	for k, v := range vals {
		f := m.GetField(k)
		if f == nil || !f.IsStored() {
			continue
		}
		setClauses = append(setClauses, fmt.Sprintf("%q = $%d", f.Column(), idx))
		args = append(args, v)
		idx++
	}

	if len(setClauses) == 0 {
		return nil
	}

	args = append(args, id)
	query := fmt.Sprintf(
		`UPDATE %q SET %s WHERE "id" = $%d`,
		m.Table(), joinStrings(setClauses, ", "), idx,
	)

	_, err := env.tx.Exec(env.ctx, query, args...)
	return err
}

func joinStrings(s []string, sep string) string {
	result := ""
	for i, str := range s {
		if i > 0 {
			result += sep
		}
		result += str
	}
	return result
}

// RunOnchangeComputes runs computed fields that depend on changed fields.
// Used by the onchange RPC to recompute values when form fields change.
// Mirrors: odoo/orm/models.py BaseModel._onchange_eval()
//
// Creates a virtual record (ID=-1) with the client's values in cache,
// then runs any compute functions whose dependencies include the changed fields.
func RunOnchangeComputes(m *Model, env *Environment, currentVals Values, changedFields []string) Values {
	result := make(Values)
	if m == nil || len(m.dependencyMap) == 0 {
		return result
	}

	// Find which computed fields need recomputation
	toRecompute := make(map[string]bool)
	for _, fname := range changedFields {
		if deps, ok := m.dependencyMap[fname]; ok {
			for _, computedField := range deps {
				toRecompute[computedField] = true
			}
		}
	}

	if len(toRecompute) == 0 {
		return result
	}

	// Populate cache with current form values so computes can read them
	fakeID := int64(-1)
	for k, v := range currentVals {
		env.cache.Set(m.Name(), fakeID, k, v)
	}
	defer env.cache.InvalidateRecord(m.Name(), fakeID)

	// Create virtual recordset
	rs := &Recordset{env: env, model: m, ids: []int64{fakeID}}

	// Track which compute functions we've already called (multiple fields can share one function)
	seen := make(map[string]bool)

	for fieldName := range toRecompute {
		f := m.GetField(fieldName)
		if f == nil || f.Compute == "" {
			continue
		}
		// Don't call the same compute function twice
		if seen[f.Compute] {
			continue
		}
		seen[f.Compute] = true

		fn, ok := m.computes[fieldName]
		if !ok {
			continue
		}

		computed, err := fn(rs)
		if err != nil {
			// Non-fatal: skip failed computes during onchange
			continue
		}
		for k, v := range computed {
			result[k] = v
		}
	}

	return result
}

// SetupAllComputes calls SetupComputes on all registered models.
func SetupAllComputes() {
	for _, m := range Registry.Models() {
		m.SetupComputes()
	}
}
