package server

import (
	"fmt"
	"time"

	"odoo-go/pkg/orm"
)

const (
	defaultWebSearchLimit  = 80
	defaultO2MFetchLimit   = 200
	defaultNameSearchLimit = 8
)

// handleWebSearchRead implements the web_search_read method.
// Mirrors: odoo/addons/web/models/models.py web_search_read()
// Returns {length: N, records: [...]} instead of just records.
func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) {
	rs := env.Model(model)

	// Parse domain from first arg (regular search_read) or kwargs (web_search_read)
	domain := parseDomain(params.Args)
	if domain == nil {
		if domainRaw, ok := params.KW["domain"].([]interface{}); ok && len(domainRaw) > 0 {
			domain = parseDomain([]interface{}{domainRaw})
		}
	}

	// Parse specification from kwargs
	spec, _ := params.KW["specification"].(map[string]interface{})
	fields := specToFields(spec)

	// Always include id
	hasID := false
	for _, f := range fields {
		if f == "id" {
			hasID = true
			break
		}
	}
	if !hasID {
		fields = append([]string{"id"}, fields...)
	}

	// Parse offset, limit, order
	offset := 0
	limit := defaultWebSearchLimit
	order := ""
	if v, ok := params.KW["offset"].(float64); ok {
		offset = int(v)
	}
	if v, ok := params.KW["limit"].(float64); ok {
		limit = int(v)
	}
	if v, ok := params.KW["order"].(string); ok {
		order = v
	}

	// Get total count, respecting count_limit for optimization.
	// Mirrors: odoo/addons/web/models/models.py web_search_read() count_limit parameter
	countLimit := int64(0)
	if v, ok := params.KW["count_limit"].(float64); ok {
		countLimit = int64(v)
	}
	count, err := rs.SearchCount(domain)
	if err != nil {
		return nil, &RPCError{Code: -32000, Message: err.Error()}
	}
	if countLimit > 0 && count > countLimit {
		count = countLimit
	}

	// Search with offset/limit
	found, err := rs.Search(domain, orm.SearchOpts{
		Offset: offset,
		Limit:  limit,
		Order:  order,
	})
	if err != nil {
		return nil, &RPCError{Code: -32000, Message: err.Error()}
	}

	var records []orm.Values
	if !found.IsEmpty() {
		records, err = found.Read(fields)
		if err != nil {
			return nil, &RPCError{Code: -32000, Message: err.Error()}
		}
	}

	formatRecordsForWeb(env, model, records, spec)

	if records == nil {
		records = []orm.Values{}
	}

	return map[string]interface{}{
		"length":  count,
		"records": records,
	}, nil
}

// handleWebRead implements the web_read method.
// Mirrors: odoo/addons/web/models/models.py web_read()
func handleWebRead(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) {
	ids := parseIDs(params.Args)
	if len(ids) == 0 {
		return []orm.Values{}, nil
	}

	spec, _ := params.KW["specification"].(map[string]interface{})
	fields := specToFields(spec)

	// Always include id
	hasID := false
	for _, f := range fields {
		if f == "id" {
			hasID = true
			break
		}
	}
	if !hasID {
		fields = append([]string{"id"}, fields...)
	}

	rs := env.Model(model)
	records, err := rs.Browse(ids...).Read(fields)
	if err != nil {
		return nil, &RPCError{Code: -32000, Message: err.Error()}
	}

	formatRecordsForWeb(env, model, records, spec)

	if records == nil {
		records = []orm.Values{}
	}
	return records, nil
}

// formatRecordsForWeb applies all web-client formatting in one call.
func formatRecordsForWeb(env *orm.Environment, model string, records []orm.Values, spec map[string]interface{}) {
	formatM2OFields(env, model, records, spec)
	formatO2MFields(env, model, records, spec)
	formatDateFields(model, records)
	normalizeNullFields(model, records)
}

// formatO2MFields fetches One2many child records and adds them to the response.
// Mirrors: odoo/addons/web/models/models.py web_read() O2M handling
func formatO2MFields(env *orm.Environment, modelName string, records []orm.Values, spec map[string]interface{}) {
	m := orm.Registry.Get(modelName)
	if m == nil || spec == nil {
		return
	}

	for fieldName, fieldSpec := range spec {
		f := m.GetField(fieldName)
		if f == nil || f.Type != orm.TypeOne2many {
			continue
		}

		// Check if spec requests sub-fields
		specMap, ok := fieldSpec.(map[string]interface{})
		if !ok {
			continue
		}
		subFieldsSpec, _ := specMap["fields"].(map[string]interface{})

		// Determine which fields to read from child model
		var childFields []string
		if len(subFieldsSpec) > 0 {
			childFields = []string{"id"}
			for name := range subFieldsSpec {
				if name != "id" {
					childFields = append(childFields, name)
				}
			}
		}

		comodel := f.Comodel
		inverseField := f.InverseField
		if comodel == "" || inverseField == "" {
			continue
		}

		// Collect all parent IDs from records
		var parentIDs []int64
		for _, rec := range records {
			parentID, ok := rec["id"].(int64)
			if !ok {
				if pid, ok := rec["id"].(int32); ok {
					parentID = int64(pid)
				}
			}
			if parentID > 0 {
				parentIDs = append(parentIDs, parentID)
			}
		}

		// Initialize all records with empty slices
		for _, rec := range records {
			rec[fieldName] = []interface{}{}
		}

		if len(parentIDs) == 0 {
			continue
		}

		// Single batched search: WHERE inverse_field IN (all parent IDs)
		childRS := env.Model(comodel)
		domain := orm.And(orm.Leaf(inverseField, "in", parentIDs))
		found, err := childRS.Search(domain, orm.SearchOpts{Limit: defaultO2MFetchLimit})
		if err != nil || found.IsEmpty() {
			continue
		}

		// Single batched read
		childRecords, err := found.Read(childFields)
		if err != nil {
			continue
		}

		// Format child records (M2O fields, dates, nulls)
		formatM2OFields(env, comodel, childRecords, subFieldsSpec)
		formatDateFields(comodel, childRecords)
		normalizeNullFields(comodel, childRecords)

		// Group child records by their inverse field (parent ID)
		grouped := make(map[int64][]interface{})
		for _, cr := range childRecords {
			if pid, ok := orm.ToRecordID(cr[inverseField]); ok && pid > 0 {
				grouped[pid] = append(grouped[pid], cr)
			}
		}

		// Assign grouped children to each parent record
		for _, rec := range records {
			parentID, ok := rec["id"].(int64)
			if !ok {
				if pid, ok := rec["id"].(int32); ok {
					parentID = int64(pid)
				}
			}
			if children, ok := grouped[parentID]; ok {
				rec[fieldName] = children
			}
		}
	}
}

// specToFields extracts field names from a specification dict.
// {"name": {}, "partner_id": {"fields": {"display_name": {}}}} → ["name", "partner_id"]
func specToFields(spec map[string]interface{}) []string {
	if len(spec) == 0 {
		return nil
	}
	fields := make([]string, 0, len(spec))
	for name := range spec {
		fields = append(fields, name)
	}
	return fields
}

// formatM2OFields converts Many2one field values from raw int to {id, display_name}.
func formatM2OFields(env *orm.Environment, modelName string, records []orm.Values, spec map[string]interface{}) {
	m := orm.Registry.Get(modelName)
	if m == nil || spec == nil {
		return
	}

	// Batch M2O resolution: collect all FK IDs per (fieldName, comodel), one NameGet per comodel
	type m2oKey struct{ field, comodel string }
	idSets := make(map[m2oKey]map[int64]bool)

	for fieldName, fieldSpec := range spec {
		f := m.GetField(fieldName)
		if f == nil || f.Type != orm.TypeMany2one || f.Comodel == "" {
			continue
		}
		if _, ok := fieldSpec.(map[string]interface{}); !ok {
			continue
		}
		key := m2oKey{fieldName, f.Comodel}
		idSets[key] = make(map[int64]bool)
		for _, rec := range records {
			if fkID, ok := orm.ToRecordID(rec[fieldName]); ok && fkID > 0 {
				idSets[key][fkID] = true
			}
		}
	}

	// One NameGet per comodel (batched)
	nameCache := make(map[m2oKey]map[int64]string)
	for key, ids := range idSets {
		if len(ids) == 0 {
			continue
		}
		var idSlice []int64
		for id := range ids {
			idSlice = append(idSlice, id)
		}
		coRS := env.Model(key.comodel).Browse(idSlice...)
		names, err := coRS.NameGet()
		if err == nil {
			nameCache[key] = names
		}
	}

	// Apply resolved names to records
	for _, rec := range records {
		for fieldName, fieldSpec := range spec {
			f := m.GetField(fieldName)
			if f == nil || f.Type != orm.TypeMany2one || f.Comodel == "" {
				continue
			}
			if _, ok := fieldSpec.(map[string]interface{}); !ok {
				continue
			}
			fkID, ok := orm.ToRecordID(rec[fieldName])
			if !ok || fkID == 0 {
				rec[fieldName] = false
				continue
			}
			key := m2oKey{fieldName, f.Comodel}
			if names, ok := nameCache[key]; ok {
				rec[fieldName] = []interface{}{fkID, names[fkID]}
			} else {
				rec[fieldName] = []interface{}{fkID, fmt.Sprintf("%s,%d", f.Comodel, fkID)}
			}
		}
	}
}

// normalizeNullFields converts SQL NULL (Go nil) to Odoo's false convention.
// Without this, the webclient renders "null" as literal text.
func normalizeNullFields(model string, records []orm.Values) {
	for _, rec := range records {
		for fieldName, val := range rec {
			if val == nil {
				rec[fieldName] = false
			}
		}
	}
}

// handleReadGroup dispatches web_read_group and read_group RPC calls.
// Mirrors: odoo/addons/web/models/models.py web_read_group() + formatted_read_group()
func (s *Server) handleReadGroup(rs *orm.Recordset, params CallKWParams) (interface{}, *RPCError) {
	// Parse domain
	domain := parseDomain(params.Args)
	if domain == nil {
		if domainRaw, ok := params.KW["domain"].([]interface{}); ok && len(domainRaw) > 0 {
			domain = parseDomain([]interface{}{domainRaw})
		}
	}

	// Parse groupby
	var groupby []string
	if gb, ok := params.KW["groupby"].([]interface{}); ok {
		for _, g := range gb {
			if s, ok := g.(string); ok {
				groupby = append(groupby, s)
			}
		}
	}

	// Parse aggregates (web client sends "fields" or "aggregates")
	var aggregates []string
	if aggs, ok := params.KW["aggregates"].([]interface{}); ok {
		for _, a := range aggs {
			if s, ok := a.(string); ok {
				aggregates = append(aggregates, s)
			}
		}
	}
	// Always include __count
	hasCount := false
	for _, a := range aggregates {
		if a == "__count" {
			hasCount = true
			break
		}
	}
	if !hasCount {
		aggregates = append(aggregates, "__count")
	}

	// Parse opts
	opts := orm.ReadGroupOpts{}
	if v, ok := params.KW["limit"].(float64); ok {
		opts.Limit = int(v)
	}
	if v, ok := params.KW["offset"].(float64); ok {
		opts.Offset = int(v)
	}
	if v, ok := params.KW["order"].(string); ok {
		opts.Order = v
	}

	if len(groupby) == 0 {
		// No groupby: return total count only (like Python Odoo)
		count, _ := rs.SearchCount(domain)
		group := map[string]interface{}{
			"__count": count,
		}
		for _, agg := range aggregates {
			if agg != "__count" {
				group[agg] = 0
			}
		}
		if params.Method == "web_read_group" {
			return map[string]interface{}{
				"groups": []interface{}{group},
				"length": 1,
			}, nil
		}
		return []interface{}{group}, nil
	}

	// Execute ReadGroup
	results, err := rs.ReadGroup(domain, groupby, aggregates, opts)
	if err != nil {
		return nil, &RPCError{Code: -32000, Message: err.Error()}
	}

	// Format results for the web client
	// Mirrors: odoo/addons/web/models/models.py _web_read_group_format()
	groups := make([]interface{}, 0, len(results))
	for _, r := range results {
		group := map[string]interface{}{
			"__extra_domain": r.Domain,
		}
		// Groupby values
		for spec, val := range r.GroupValues {
			group[spec] = val
		}
		// Aggregate values
		for spec, val := range r.AggValues {
			group[spec] = val
		}
		// Ensure __count
		if _, ok := group["__count"]; !ok {
			group["__count"] = r.Count
		}
		groups = append(groups, group)
	}

	if groups == nil {
		groups = []interface{}{}
	}

	if params.Method == "web_read_group" {
		// web_read_group: also get total group count (without limit/offset)
		totalLen := len(results)
		if opts.Limit > 0 || opts.Offset > 0 {
			allResults, err := rs.ReadGroup(domain, groupby, []string{"__count"})
			if err == nil {
				totalLen = len(allResults)
			}
		}

		return map[string]interface{}{
			"groups": groups,
			"length": totalLen,
		}, nil
	}

	// Legacy read_group format
	return groups, nil
}

// formatDateFields converts date/datetime values to Odoo's expected string format.
func formatDateFields(model string, records []orm.Values) {
	m := orm.Registry.Get(model)
	if m == nil {
		return
	}
	for _, rec := range records {
		for fieldName, val := range rec {
			f := m.GetField(fieldName)
			if f == nil {
				continue
			}
			if f.Type == orm.TypeDate || f.Type == orm.TypeDatetime {
				switch v := val.(type) {
				case time.Time:
					if f.Type == orm.TypeDate {
						rec[fieldName] = v.Format("2006-01-02")
					} else {
						rec[fieldName] = v.Format("2006-01-02 15:04:05")
					}
				case string:
					// Already a string, might need reformatting
					if t, err := time.Parse(time.RFC3339, v); err == nil {
						if f.Type == orm.TypeDate {
							rec[fieldName] = t.Format("2006-01-02")
						} else {
							rec[fieldName] = t.Format("2006-01-02 15:04:05")
						}
					}
				}
			}
			// Also convert boolean fields: Go nil → Odoo false
			if f.Type == orm.TypeBoolean && val == nil {
				rec[fieldName] = false
			}
		}
	}
}
