package orm

import (
	"fmt"
	"strings"
)

// Model defines an Odoo model (database table + business logic).
// Mirrors: odoo/orm/models.py BaseModel
//
// Odoo:
//
//	class ResPartner(models.Model):
//	    _name = 'res.partner'
//	    _description = 'Contact'
//	    _order = 'name'
//	    _rec_name = 'name'
//
// Go:
//
//	NewModel("res.partner", ModelOpts{
//	    Description: "Contact",
//	    Order: "name",
//	    RecName: "name",
//	})
type Model struct {
	name        string
	description string
	modelType   ModelType
	table       string    // SQL table name
	order       string    // Default sort order
	recName     string    // Field used as display name
	inherit     []string  // _inherit: models to extend
	inherits    map[string]string // _inherits: {parent_model: fk_field}
	auto        bool      // Auto-create/update table schema
	logAccess   bool      // Auto-create create_uid, write_uid, create_date, write_date

	// Fields
	fields map[string]*Field

	// Methods (registered business logic)
	methods map[string]interface{}

	// Access control
	checkCompany bool // Enforce multi-company record rules

	// Hooks
	BeforeCreate func(env *Environment, vals Values) error        // Called before INSERT
	DefaultGet   func(env *Environment, fields []string) Values   // Dynamic defaults (e.g., from DB)
	Constraints  []ConstraintFunc                                 // Validation constraints
	Methods      map[string]MethodFunc                            // Named business methods

	// SQL-level constraints
	SQLConstraints []SQLConstraint

	// Computed fields
	computes      map[string]ComputeFunc // field_name → compute function
	dependencyMap map[string][]string    // trigger_field → []computed_field_names

	// Onchange handlers
	// Maps field_name → handler that receives current vals and returns computed updates.
	// Mirrors: @api.onchange in Odoo.
	OnchangeHandlers map[string]func(env *Environment, vals Values) Values

	// Resolved
	parents    []*Model          // Resolved parent models from _inherit
	allFields  map[string]*Field // Including fields from parents
	fieldOrder []string          // Field creation order
}

// ModelOpts configures a new model.
type ModelOpts struct {
	Description  string
	Type         ModelType
	Table        string
	Order        string
	RecName      string
	Inherit      []string
	Inherits     map[string]string
	CheckCompany bool
}

// NewModel creates and registers a new model.
// Mirrors: class MyModel(models.Model): _name = '...'
func NewModel(name string, opts ModelOpts) *Model {
	m := &Model{
		name:         name,
		description:  opts.Description,
		modelType:    opts.Type,
		table:        opts.Table,
		order:        opts.Order,
		recName:      opts.RecName,
		inherit:      opts.Inherit,
		inherits:     opts.Inherits,
		checkCompany: opts.CheckCompany,
		auto:         opts.Type != ModelAbstract,
		logAccess:    true,
		fields:       make(map[string]*Field),
		methods:      make(map[string]interface{}),
		allFields:    make(map[string]*Field),
	}

	// Derive table name from model name if not specified.
	// Mirrors: odoo/orm/models.py BaseModel._table
	// 'res.partner' → 'res_partner'
	if m.table == "" && m.auto {
		m.table = strings.ReplaceAll(name, ".", "_")
	}

	if m.order == "" {
		m.order = "id"
	}

	if m.recName == "" {
		m.recName = "name"
	}

	// Add magic fields.
	// Mirrors: odoo/orm/models.py BaseModel.MAGIC_COLUMNS
	m.addMagicFields()

	Registry.Register(m)
	return m
}

// addMagicFields adds Odoo's automatic fields to every model.
// Mirrors: odoo/orm/models.py MAGIC_COLUMNS + LOG_ACCESS_COLUMNS
func (m *Model) addMagicFields() {
	// id is always present
	m.AddField(Integer("id", FieldOpts{
		String:   "ID",
		Readonly: true,
	}))

	// display_name: computed on-the-fly from rec_name, not stored in DB.
	// Mirrors: odoo/orm/models.py BaseModel.display_name (computed field on ALL models)
	m.AddField(Char("display_name", FieldOpts{
		String:  "Display Name",
		Compute: "_compute_display_name",
	}))

	if m.logAccess {
		m.AddField(Many2one("create_uid", "res.users", FieldOpts{
			String:   "Created by",
			Readonly: true,
		}))
		m.AddField(Datetime("create_date", FieldOpts{
			String:   "Created on",
			Readonly: true,
		}))
		m.AddField(Many2one("write_uid", "res.users", FieldOpts{
			String:   "Last Updated by",
			Readonly: true,
		}))
		m.AddField(Datetime("write_date", FieldOpts{
			String:   "Last Updated on",
			Readonly: true,
		}))
	}
}

// AddField adds a field to this model.
func (m *Model) AddField(f *Field) *Model {
	f.model = m
	m.fields[f.Name] = f
	m.allFields[f.Name] = f
	m.fieldOrder = append(m.fieldOrder, f.Name)
	return m
}

// AddFields adds multiple fields at once.
func (m *Model) AddFields(fields ...*Field) *Model {
	for _, f := range fields {
		m.AddField(f)
	}
	return m
}

// GetField returns a field by name, or nil.
func (m *Model) GetField(name string) *Field {
	return m.allFields[name]
}

// Fields returns all fields of this model (including inherited).
func (m *Model) Fields() map[string]*Field {
	return m.allFields
}

// StoredFields returns fields that have a database column.
func (m *Model) StoredFields() []*Field {
	var result []*Field
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if f.IsStored() {
			result = append(result, f)
		}
	}
	return result
}

// Name returns the model name (e.g., "res.partner").
func (m *Model) Name() string { return m.name }

// Table returns the SQL table name (e.g., "res_partner").
func (m *Model) Table() string { return m.table }

// Description returns the human-readable model description.
func (m *Model) Description() string { return m.description }

// Order returns the default sort expression.
func (m *Model) Order() string { return m.order }

// RecName returns the field name used as display name.
func (m *Model) RecName() string { return m.recName }

// IsAbstract returns true if this model has no database table.
func (m *Model) IsAbstract() bool { return m.modelType == ModelAbstract }

// IsTransient returns true if this is a wizard/temporary model.
func (m *Model) IsTransient() bool { return m.modelType == ModelTransient }

// SQLConstraint represents a SQL-level constraint on a model's table.
// Mirrors: _sql_constraints in Odoo.
type SQLConstraint struct {
	Name       string // Constraint name
	Definition string // SQL definition (e.g., "UNIQUE(name, company_id)")
	Message    string // Error message
}

// AddSQLConstraint registers a SQL-level constraint on this model.
func (m *Model) AddSQLConstraint(name, definition, message string) {
	m.SQLConstraints = append(m.SQLConstraints, SQLConstraint{Name: name, Definition: definition, Message: message})
}

// ConstraintFunc validates a recordset. Returns error if constraint violated.
// Mirrors: @api.constrains in Odoo.
type ConstraintFunc func(rs *Recordset) error

// MethodFunc is a callable business method on a model.
// Mirrors: Odoo model methods called via RPC.
type MethodFunc func(rs *Recordset, args ...interface{}) (interface{}, error)

// AddConstraint registers a validation constraint.
func (m *Model) AddConstraint(fn ConstraintFunc) *Model {
	m.Constraints = append(m.Constraints, fn)
	return m
}

// RegisterMethod registers a named business logic method on this model.
// Mirrors: Odoo's method definitions on model classes.
func (m *Model) RegisterMethod(name string, fn MethodFunc) *Model {
	if m.Methods == nil {
		m.Methods = make(map[string]MethodFunc)
	}
	m.Methods[name] = fn
	return m
}

// RegisterOnchange registers an onchange handler for a field.
// When the field changes on the client, the handler is called with the current
// record values and returns computed field updates.
// Mirrors: @api.onchange('field_name') in Odoo.
func (m *Model) RegisterOnchange(fieldName string, handler func(env *Environment, vals Values) Values) {
	if m.OnchangeHandlers == nil {
		m.OnchangeHandlers = make(map[string]func(env *Environment, vals Values) Values)
	}
	m.OnchangeHandlers[fieldName] = handler
}

// Extend extends this model with additional fields (like _inherit in Odoo).
// Mirrors: class MyModelExt(models.Model): _inherit = 'res.partner'
func (m *Model) Extend(fields ...*Field) *Model {
	for _, f := range fields {
		m.AddField(f)
	}
	return m
}

// ExtendModel retrieves an existing model for extension by another module.
// Mirrors: Python Odoo's _inherit = 'model.name' (without _name).
//
// This is the formal mechanism for cross-module model extension. It provides:
//  1. Explicit intent (extending vs creating)
//  2. Panic on missing model (catch registration order bugs early)
//  3. Python Odoo compatibility pattern
//
// Usage:
//
//	m := orm.ExtendModel("res.partner")
//	m.AddFields(orm.Char("industry", orm.FieldOpts{String: "Industry"}))
//	m.RegisterMethod("action_custom", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { ... })
func ExtendModel(name string) *Model {
	m := Registry.Get(name)
	if m == nil {
		panic(fmt.Sprintf("orm: cannot extend unregistered model %q", name))
	}
	return m
}

// RegisterInverse registers an inverse method for a computed field.
// The method is auto-named "_inverse_<fieldName>" and linked to the field's
// Inverse property so that Write() calls it automatically.
// Mirrors: odoo/orm/fields.py Field.inverse
func (m *Model) RegisterInverse(fieldName string, fn MethodFunc) {
	inverseName := "_inverse_" + fieldName
	m.RegisterMethod(inverseName, fn)
	if f := m.GetField(fieldName); f != nil {
		f.Inverse = inverseName
	}
}

// CreateTableSQL generates the CREATE TABLE statement for this model.
// Mirrors: odoo/orm/models.py BaseModel._table_exist / init()
func (m *Model) CreateTableSQL() string {
	if !m.auto {
		return ""
	}

	var cols []string
	cols = append(cols, `"id" SERIAL PRIMARY KEY`)

	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if name == "id" || !f.IsStored() {
			continue
		}

		sqlType := f.SQLType()
		if sqlType == "" {
			continue
		}

		col := fmt.Sprintf("    %q %s", f.Column(), sqlType)
		if f.Required {
			col += " NOT NULL"
		}
		cols = append(cols, col)
	}

	return fmt.Sprintf("CREATE TABLE IF NOT EXISTS %q (\n%s\n)",
		m.table,
		strings.Join(cols, ",\n"),
	)
}

// ForeignKeySQL generates ALTER TABLE statements for foreign keys.
func (m *Model) ForeignKeySQL() []string {
	var stmts []string
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if f.Type != TypeMany2one || f.Comodel == "" {
			continue
		}
		if name == "create_uid" || name == "write_uid" {
			continue // Skip magic fields, added later
		}
		comodel := Registry.Get(f.Comodel)
		if comodel == nil {
			continue
		}
		onDelete := "SET NULL"
		switch f.OnDelete {
		case OnDeleteRestrict:
			onDelete = "RESTRICT"
		case OnDeleteCascade:
			onDelete = "CASCADE"
		}
		stmt := fmt.Sprintf(
			`ALTER TABLE %q ADD CONSTRAINT %q FOREIGN KEY (%q) REFERENCES %q ("id") ON DELETE %s`,
			m.table,
			fmt.Sprintf("%s_%s_fkey", m.table, f.Column()),
			f.Column(),
			comodel.Table(),
			onDelete,
		)
		stmts = append(stmts, stmt)
	}
	return stmts
}

// IndexSQL generates CREATE INDEX statements.
func (m *Model) IndexSQL() []string {
	var stmts []string
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if !f.Index || !f.IsStored() || name == "id" {
			continue
		}
		stmt := fmt.Sprintf(
			`CREATE INDEX IF NOT EXISTS %q ON %q (%q)`,
			fmt.Sprintf("%s_%s_index", m.table, f.Column()),
			m.table,
			f.Column(),
		)
		stmts = append(stmts, stmt)
	}
	return stmts
}

// Many2manyTableSQL generates junction table DDL for Many2many fields.
func (m *Model) Many2manyTableSQL() []string {
	var stmts []string
	for _, name := range m.fieldOrder {
		f := m.fields[name]
		if f.Type != TypeMany2many {
			continue
		}

		comodel := Registry.Get(f.Comodel)
		if comodel == nil {
			continue
		}

		// Determine junction table name.
		// Mirrors: odoo/orm/fields_relational.py Many2many._table_name
		relation := f.Relation
		if relation == "" {
			// Default: alphabetically sorted model tables joined by underscore
			t1, t2 := m.table, comodel.Table()
			if t1 > t2 {
				t1, t2 = t2, t1
			}
			relation = fmt.Sprintf("%s_%s_rel", t1, t2)
		}

		col1 := f.Column1
		if col1 == "" {
			col1 = m.table + "_id"
		}
		col2 := f.Column2
		if col2 == "" {
			col2 = comodel.Table() + "_id"
		}
		// Self-referential M2M: ensure distinct column names
		if col1 == col2 {
			col1 = m.table + "_src_id"
			col2 = m.table + "_dst_id"
		}

		stmt := fmt.Sprintf(
			"CREATE TABLE IF NOT EXISTS %q (\n"+
				"    %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+
				"    %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+
				"    PRIMARY KEY (%q, %q)\n"+
				")",
			relation, col1, m.table, col2, comodel.Table(), col1, col2,
		)
		stmts = append(stmts, stmt)
	}
	return stmts
}
