package orm

import (
	"fmt"
	"time"
)

// Field defines a model field.
// Mirrors: odoo/orm/fields.py Field class
//
// Odoo field declaration:
//
//	name = fields.Char(string='Name', required=True, index=True)
//
// Go equivalent:
//
//	Char("name", FieldOpts{String: "Name", Required: true, Index: true})
type Field struct {
	Name   string    // Technical name (e.g., "name", "partner_id")
	Type   FieldType // Field type (TypeChar, TypeMany2one, etc.)
	String string    // Human-readable label

	// Constraints
	Required bool
	Readonly bool
	Index    bool   // Create database index
	Unique   bool   // Unique constraint (not in Odoo, but useful)
	Size     int    // Max length for Char fields
	Default  Value  // Default value or function name
	Help     string // Tooltip / help text

	// Computed fields
	// Mirrors: odoo/orm/fields.py Field.compute, Field.inverse, Field.depends
	Compute  string   // Method name to compute this field
	Inverse  string   // Method name to write back computed value
	Depends  []string // Field names this compute depends on
	Store    bool     // Store computed value in DB (default: false for computed)
	Precompute bool   // Compute before first DB flush

	// Related fields
	// Mirrors: odoo/orm/fields.py Field.related
	Related string // Dot-separated path (e.g., "partner_id.name")

	// Selection
	Selection []SelectionItem

	// Relational fields
	// Mirrors: odoo/orm/fields_relational.py
	Comodel      string   // Target model name (e.g., "res.company")
	InverseField string   // For One2many: field name in comodel pointing back
	Relation     string   // For Many2many: junction table name
	Column1      string   // For Many2many: column name for this model's FK
	Column2      string   // For Many2many: column name for comodel's FK
	Domain       Domain   // Default domain filter for relational fields
	OnDelete     OnDelete // For Many2one: what happens on target deletion

	// Monetary
	CurrencyField string // Field name holding the currency_id

	// Translation
	Translate bool // Field supports multi-language

	// Copy
	Copy bool // Whether this field is copied on record duplication

	// Groups (access control)
	Groups string // Comma-separated group XML IDs

	// Internal
	model  *Model // Back-reference to owning model
	column string // SQL column name (usually same as Name)
}

// Column returns the SQL column name for this field.
func (f *Field) Column() string {
	if f.column != "" {
		return f.column
	}
	return f.Name
}

// SQLType returns the PostgreSQL type for this field.
func (f *Field) SQLType() string {
	if f.Type == TypeChar && f.Size > 0 {
		return fmt.Sprintf("varchar(%d)", f.Size)
	}
	return f.Type.SQLType()
}

// IsStored returns true if this field has a database column.
func (f *Field) IsStored() bool {
	// Computed fields without Store are not stored
	if f.Compute != "" && !f.Store {
		return false
	}
	// Related fields without Store are not stored
	if f.Related != "" && !f.Store {
		return false
	}
	return f.Type.IsStored()
}

// IsCopyable returns true if this field should be copied on record duplication.
// Mirrors: odoo/orm/fields.py Field.copy
func (f *Field) IsCopyable() bool {
	// If explicitly set, use that
	if f.Copy {
		return true
	}
	// Defaults: non-copyable for computed, O2M, id, timestamps
	if f.Name == "id" || f.Name == "create_uid" || f.Name == "create_date" ||
		f.Name == "write_uid" || f.Name == "write_date" || f.Name == "password" {
		return false
	}
	if f.Compute != "" && !f.Store {
		return false
	}
	if f.Type == TypeOne2many {
		return false
	}
	return true
}

// IsRelational returns true for relational field types.
func (f *Field) IsRelational() bool {
	return f.Type.IsRelational()
}

// --- Field constructors ---
// Mirror Odoo's fields.Char(), fields.Integer(), etc.

// FieldOpts holds optional parameters for field constructors.
type FieldOpts struct {
	String        string
	Required      bool
	Readonly      bool
	Index         bool
	Size          int
	Default       Value
	Help          string
	Compute       string
	Inverse       string
	Depends       []string
	Store         bool
	Precompute    bool
	Related       string
	Selection     []SelectionItem
	Comodel       string
	InverseField  string
	Relation      string
	Column1       string
	Column2       string
	Domain        Domain
	OnDelete      OnDelete
	CurrencyField string
	Translate     bool
	Copy          bool
	Groups        string
}

func newField(name string, typ FieldType, opts FieldOpts) *Field {
	f := &Field{
		Name:          name,
		Type:          typ,
		String:        opts.String,
		Required:      opts.Required,
		Readonly:      opts.Readonly,
		Index:         opts.Index,
		Size:          opts.Size,
		Default:       opts.Default,
		Help:          opts.Help,
		Compute:       opts.Compute,
		Inverse:       opts.Inverse,
		Depends:       opts.Depends,
		Store:         opts.Store,
		Precompute:    opts.Precompute,
		Related:       opts.Related,
		Selection:     opts.Selection,
		Comodel:       opts.Comodel,
		InverseField:  opts.InverseField,
		Relation:      opts.Relation,
		Column1:       opts.Column1,
		Column2:       opts.Column2,
		Domain:        opts.Domain,
		OnDelete:      opts.OnDelete,
		CurrencyField: opts.CurrencyField,
		Translate:     opts.Translate,
		Copy:          opts.Copy,
		Groups:        opts.Groups,
		column:        name,
	}
	if f.String == "" {
		f.String = name
	}
	return f
}

// Char creates a character field. Mirrors: fields.Char
func Char(name string, opts FieldOpts) *Field {
	return newField(name, TypeChar, opts)
}

// Text creates a text field. Mirrors: fields.Text
func Text(name string, opts FieldOpts) *Field {
	return newField(name, TypeText, opts)
}

// HTML creates an HTML field. Mirrors: fields.Html
func HTML(name string, opts FieldOpts) *Field {
	return newField(name, TypeHTML, opts)
}

// Integer creates an integer field. Mirrors: fields.Integer
func Integer(name string, opts FieldOpts) *Field {
	return newField(name, TypeInteger, opts)
}

// Float creates a float field. Mirrors: fields.Float
func Float(name string, opts FieldOpts) *Field {
	return newField(name, TypeFloat, opts)
}

// Monetary creates a monetary field. Mirrors: fields.Monetary
func Monetary(name string, opts FieldOpts) *Field {
	return newField(name, TypeMonetary, opts)
}

// Boolean creates a boolean field. Mirrors: fields.Boolean
func Boolean(name string, opts FieldOpts) *Field {
	return newField(name, TypeBoolean, opts)
}

// Date creates a date field. Mirrors: fields.Date
func Date(name string, opts FieldOpts) *Field {
	return newField(name, TypeDate, opts)
}

// Datetime creates a datetime field. Mirrors: fields.Datetime
func Datetime(name string, opts FieldOpts) *Field {
	return newField(name, TypeDatetime, opts)
}

// Binary creates a binary field. Mirrors: fields.Binary
func Binary(name string, opts FieldOpts) *Field {
	return newField(name, TypeBinary, opts)
}

// Selection creates a selection field. Mirrors: fields.Selection
func Selection(name string, selection []SelectionItem, opts FieldOpts) *Field {
	opts.Selection = selection
	return newField(name, TypeSelection, opts)
}

// Json creates a JSON field. Mirrors: fields.Json
func Json(name string, opts FieldOpts) *Field {
	return newField(name, TypeJson, opts)
}

// Many2one creates a many-to-one relational field. Mirrors: fields.Many2one
func Many2one(name string, comodel string, opts FieldOpts) *Field {
	opts.Comodel = comodel
	if opts.OnDelete == "" {
		opts.OnDelete = OnDeleteSetNull
	}
	f := newField(name, TypeMany2one, opts)
	f.Index = true // M2O fields are always indexed in Odoo
	return f
}

// One2many creates a one-to-many relational field. Mirrors: fields.One2many
func One2many(name string, comodel string, inverseField string, opts FieldOpts) *Field {
	opts.Comodel = comodel
	opts.InverseField = inverseField
	return newField(name, TypeOne2many, opts)
}

// Many2many creates a many-to-many relational field. Mirrors: fields.Many2many
func Many2many(name string, comodel string, opts FieldOpts) *Field {
	opts.Comodel = comodel
	return newField(name, TypeMany2many, opts)
}

// --- Default & Validation Helpers ---

// ResolveDefault returns the concrete default value for this field.
// Handles: literal values, "today" sentinel for date/datetime fields.
func (f *Field) ResolveDefault() Value {
	if f.Default == nil {
		return nil
	}

	switch v := f.Default.(type) {
	case string:
		if v == "today" && (f.Type == TypeDate || f.Type == TypeDatetime) {
			return time.Now().Format("2006-01-02")
		}
		return v
	case bool, int, int64, float64:
		return v
	default:
		return v
	}
}

// ApplyDefaults sets default values on vals for any stored field that is
// missing from vals and has a non-nil Default.
// Mirrors: odoo/orm/models.py BaseModel._add_missing_default_values()
func ApplyDefaults(m *Model, vals Values) {
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if name == "id" || !f.IsStored() || f.Default == nil {
			continue
		}
		if _, exists := vals[name]; exists {
			continue
		}
		if resolved := f.ResolveDefault(); resolved != nil {
			vals[name] = resolved
		}
	}
}

// ValidateRequired checks that all required stored fields have a non-nil value in vals.
// Returns an error describing the first missing required field.
// Mirrors: odoo/orm/models.py BaseModel._check_required()
func ValidateRequired(m *Model, vals Values, isCreate bool) error {
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if !f.Required || !f.IsStored() || name == "id" {
			continue
		}
		// Magic fields are auto-set
		if name == "create_uid" || name == "write_uid" || name == "create_date" || name == "write_date" {
			continue
		}
		if isCreate {
			val, exists := vals[name]
			if !exists || val == nil {
				return fmt.Errorf("orm: field '%s' is required on %s", name, m.Name())
			}
		}
		// On write: only check if the field is explicitly set to nil
		if !isCreate {
			val, exists := vals[name]
			if exists && val == nil {
				return fmt.Errorf("orm: field '%s' cannot be set to null on %s (required)", name, m.Name())
			}
		}
	}
	return nil
}

// JunctionTable returns the M2M junction table name for this field.
func (f *Field) JunctionTable() string {
	if f.Relation != "" {
		return f.Relation
	}
	if f.model == nil || f.Comodel == "" {
		return ""
	}
	comodel := Registry.Get(f.Comodel)
	if comodel == nil {
		return ""
	}
	t1, t2 := f.model.Table(), comodel.Table()
	if t1 > t2 {
		t1, t2 = t2, t1
	}
	return fmt.Sprintf("%s_%s_rel", t1, t2)
}

// JunctionCol1 returns this model's FK column in the junction table.
func (f *Field) JunctionCol1() string {
	if f.Column1 != "" {
		return f.Column1
	}
	if f.model == nil {
		return ""
	}
	col := f.model.Table() + "_id"
	// Self-referential
	comodel := Registry.Get(f.Comodel)
	if comodel != nil && f.model.Table() == comodel.Table() {
		col = f.model.Table() + "_src_id"
	}
	return col
}

// JunctionCol2 returns the comodel's FK column in the junction table.
func (f *Field) JunctionCol2() string {
	if f.Column2 != "" {
		return f.Column2
	}
	comodel := Registry.Get(f.Comodel)
	if comodel == nil {
		return ""
	}
	col := comodel.Table() + "_id"
	if f.model != nil && f.model.Table() == comodel.Table() {
		col = comodel.Table() + "_dst_id"
	}
	return col
}
