package orm

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// Environment is the central context for all ORM operations.
// Mirrors: odoo/orm/environments.py Environment
//
// Odoo: self.env['res.partner'].search([('name', 'ilike', 'test')])
// Go:   env.Model("res.partner").Search(And(Leaf("name", "ilike", "test")))
//
// An Environment wraps:
// - A database transaction (cursor in Odoo terms)
// - The current user ID
// - The current company ID
// - Context values (lang, tz, etc.)
type Environment struct {
	ctx       context.Context
	pool      *pgxpool.Pool
	tx        pgx.Tx
	uid       int64
	companyID int64
	su        bool // sudo mode (bypass access checks)
	context   map[string]interface{}
	cache     *Cache
}

// EnvConfig configures a new Environment.
type EnvConfig struct {
	Pool      *pgxpool.Pool
	UID       int64
	CompanyID int64
	Context   map[string]interface{}
}

// NewEnvironment creates a new ORM environment.
// Mirrors: odoo.api.Environment(cr, uid, context)
func NewEnvironment(ctx context.Context, cfg EnvConfig) (*Environment, error) {
	tx, err := cfg.Pool.Begin(ctx)
	if err != nil {
		return nil, fmt.Errorf("orm: begin transaction: %w", err)
	}

	envCtx := cfg.Context
	if envCtx == nil {
		envCtx = make(map[string]interface{})
	}

	return &Environment{
		ctx:       ctx,
		pool:      cfg.Pool,
		tx:        tx,
		uid:       cfg.UID,
		companyID: cfg.CompanyID,
		context:   envCtx,
		cache:     NewCache(),
	}, nil
}

// Model returns a Recordset bound to this environment for the given model.
// Mirrors: self.env['model.name']
func (env *Environment) Model(name string) *Recordset {
	m := Registry.MustGet(name)
	return &Recordset{
		env:   env,
		model: m,
		ids:   nil,
	}
}

// Ref returns a record by its XML ID (external identifier).
// Mirrors: self.env.ref('module.xml_id')
func (env *Environment) Ref(xmlID string) (*Recordset, error) {
	// Try direct integer ID (for programmatic use) — reject since model is unknown
	if id, err := strconv.ParseInt(xmlID, 10, 64); err == nil {
		return nil, fmt.Errorf("orm: ref requires module.name format, not bare ID %d", id)
	}

	// Query ir_model_data for the external ID
	var resModel string
	var resID int64

	parts := strings.SplitN(xmlID, ".", 2)
	if len(parts) != 2 {
		return nil, fmt.Errorf("orm: ref %q must be module.name format", xmlID)
	}

	err := env.tx.QueryRow(env.ctx,
		`SELECT model, res_id FROM ir_model_data WHERE module = $1 AND name = $2 LIMIT 1`,
		parts[0], parts[1],
	).Scan(&resModel, &resID)
	if err != nil {
		return nil, fmt.Errorf("orm: ref %q not found: %w", xmlID, err)
	}

	return env.Model(resModel).Browse(resID), nil
}

// UID returns the current user ID.
func (env *Environment) UID() int64 { return env.uid }

// CompanyID returns the current company ID.
func (env *Environment) CompanyID() int64 { return env.companyID }

// IsSuperuser returns true if this environment bypasses access checks.
func (env *Environment) IsSuperuser() bool { return env.su }

// Context returns the environment context (Odoo-style key-value context).
func (env *Environment) Context() map[string]interface{} { return env.context }

// Ctx returns the Go context.Context for database operations.
func (env *Environment) Ctx() context.Context { return env.ctx }

// Lang returns the language from context.
func (env *Environment) Lang() string {
	if lang, ok := env.context["lang"].(string); ok {
		return lang
	}
	return "en_US"
}

// Tx returns the underlying database transaction.
func (env *Environment) Tx() pgx.Tx { return env.tx }

// Sudo returns a new environment with superuser privileges.
// Mirrors: self.env['model'].sudo()
func (env *Environment) Sudo() *Environment {
	return &Environment{
		ctx:       env.ctx,
		pool:      env.pool,
		tx:        env.tx,
		uid:       env.uid,
		companyID: env.companyID,
		su:        true,
		context:   env.context,
		cache:     env.cache,
	}
}

// WithUser returns a new environment for a different user.
// Mirrors: self.with_user(user_id)
func (env *Environment) WithUser(uid int64) *Environment {
	return &Environment{
		ctx:       env.ctx,
		pool:      env.pool,
		tx:        env.tx,
		uid:       uid,
		companyID: env.companyID,
		su:        false,
		context:   env.context,
		cache:     env.cache,
	}
}

// WithCompany returns a new environment for a different company.
// Mirrors: self.with_company(company_id)
func (env *Environment) WithCompany(companyID int64) *Environment {
	return &Environment{
		ctx:       env.ctx,
		pool:      env.pool,
		tx:        env.tx,
		uid:       env.uid,
		companyID: companyID,
		su:        env.su,
		context:   env.context,
		cache:     env.cache,
	}
}

// WithContext returns a new environment with additional context values.
// Mirrors: self.with_context(key=value)
func (env *Environment) WithContext(extra map[string]interface{}) *Environment {
	merged := make(map[string]interface{}, len(env.context)+len(extra))
	for k, v := range env.context {
		merged[k] = v
	}
	for k, v := range extra {
		merged[k] = v
	}
	return &Environment{
		ctx:       env.ctx,
		pool:      env.pool,
		tx:        env.tx,
		uid:       env.uid,
		companyID: env.companyID,
		su:        env.su,
		context:   merged,
		cache:     env.cache,
	}
}

// Commit commits the database transaction.
func (env *Environment) Commit() error {
	return env.tx.Commit(env.ctx)
}

// Rollback rolls back the database transaction.
func (env *Environment) Rollback() error {
	return env.tx.Rollback(env.ctx)
}

// Close rolls back uncommitted changes and releases the connection.
func (env *Environment) Close() error {
	return env.tx.Rollback(env.ctx)
}

// --- Cache ---

// Cache is a per-environment record cache.
// Mirrors: odoo/orm/environments.py Transaction cache
type Cache struct {
	// model_name → record_id → field_name → value
	data map[string]map[int64]map[string]Value
}

// NewCache creates an empty cache.
func NewCache() *Cache {
	return &Cache{
		data: make(map[string]map[int64]map[string]Value),
	}
}

// Get retrieves a cached value.
func (c *Cache) Get(model string, id int64, field string) (Value, bool) {
	records, ok := c.data[model]
	if !ok {
		return nil, false
	}
	fields, ok := records[id]
	if !ok {
		return nil, false
	}
	val, ok := fields[field]
	return val, ok
}

// Set stores a value in the cache.
func (c *Cache) Set(model string, id int64, field string, value Value) {
	records, ok := c.data[model]
	if !ok {
		records = make(map[int64]map[string]Value)
		c.data[model] = records
	}
	fields, ok := records[id]
	if !ok {
		fields = make(map[string]Value)
		records[id] = fields
	}
	fields[field] = value
}

// Invalidate removes all cached values for a model.
func (c *Cache) Invalidate(model string) {
	delete(c.data, model)
}

// InvalidateRecord removes cached values for a specific record.
func (c *Cache) InvalidateRecord(model string, id int64) {
	if records, ok := c.data[model]; ok {
		delete(records, id)
	}
}
