package orm

import (
	"fmt"
	"strconv"
	"strings"
	"unicode"
)

// ParseDomainString parses a Python-style domain_force string into an orm.Domain.
// Mirrors: odoo/addons/base/models/ir_rule.py safe_eval(domain_force, eval_context)
//
// Supported syntax:
//   - Tuples: ('field', 'operator', value)
//   - Logical operators: '&', '|', '!'
//   - Values: string literals, numbers, True/False, None, list literals, context variables
//   - Context variables: user.id, company_id, user.company_id, company_ids
//
// The env parameter provides runtime context for variable resolution.
func ParseDomainString(s string, env *Environment) (Domain, error) {
	s = strings.TrimSpace(s)
	if s == "" || s == "[]" {
		return nil, nil
	}

	p := &domainParser{
		input: []rune(s),
		pos:   0,
		env:   env,
	}

	return p.parseDomain()
}

// domainParser is a simple recursive-descent parser for Python domain expressions.
type domainParser struct {
	input []rune
	pos   int
	env   *Environment
}

func (p *domainParser) parseDomain() (Domain, error) {
	p.skipWhitespace()
	if p.pos >= len(p.input) {
		return nil, nil
	}

	if p.input[p.pos] != '[' {
		return nil, fmt.Errorf("domain_parse: expected '[' at position %d, got %c", p.pos, p.input[p.pos])
	}
	p.pos++ // consume '['

	var nodes []DomainNode

	for {
		p.skipWhitespace()
		if p.pos >= len(p.input) {
			return nil, fmt.Errorf("domain_parse: unexpected end of input")
		}
		if p.input[p.pos] == ']' {
			p.pos++ // consume ']'
			break
		}

		// Skip commas between elements
		if p.input[p.pos] == ',' {
			p.pos++
			continue
		}

		node, err := p.parseNode()
		if err != nil {
			return nil, err
		}
		nodes = append(nodes, node)
	}

	// Convert the list of nodes into a proper Domain.
	// Odoo domains are in prefix (Polish) notation:
	//   ['&', (a), (b)]  means  a AND b
	// If no explicit operator prefix, Odoo implicitly ANDs consecutive leaves.
	return normalizeDomainNodes(nodes), nil
}

// normalizeDomainNodes adds implicit '&' operators between consecutive leaf nodes
// that don't have an explicit operator, mirroring Odoo's behavior.
func normalizeDomainNodes(nodes []DomainNode) Domain {
	if len(nodes) == 0 {
		return nil
	}

	// Check if the domain already has operators in prefix position.
	// If first node is an operator, assume the domain is already in Polish notation.
	if _, isOp := nodes[0].(Operator); isOp {
		return Domain(nodes)
	}

	// No prefix operators: implicitly AND all leaf conditions.
	if len(nodes) == 1 {
		return Domain{nodes[0]}
	}

	// Multiple leaves without operators: AND them together.
	return And(nodes...)
}

func (p *domainParser) parseNode() (DomainNode, error) {
	p.skipWhitespace()
	if p.pos >= len(p.input) {
		return nil, fmt.Errorf("domain_parse: unexpected end of input")
	}

	ch := p.input[p.pos]

	// Check for logical operators: '&', '|', '!'
	if ch == '\'' || ch == '"' {
		// Could be a string operator like '&' or '|' or '!'
		str, err := p.parseString()
		if err != nil {
			return nil, err
		}
		switch str {
		case "&":
			return OpAnd, nil
		case "|":
			return OpOr, nil
		case "!":
			return OpNot, nil
		default:
			return nil, fmt.Errorf("domain_parse: unexpected string %q where operator or tuple expected", str)
		}
	}

	// Check for tuple: (field, operator, value)
	if ch == '(' {
		return p.parseTuple()
	}

	return nil, fmt.Errorf("domain_parse: unexpected character %c at position %d", ch, p.pos)
}

func (p *domainParser) parseTuple() (DomainNode, error) {
	if p.input[p.pos] != '(' {
		return nil, fmt.Errorf("domain_parse: expected '(' at position %d", p.pos)
	}
	p.pos++ // consume '('

	// Parse field name (string)
	p.skipWhitespace()
	field, err := p.parseString()
	if err != nil {
		return nil, fmt.Errorf("domain_parse: field name: %w", err)
	}

	p.skipWhitespace()
	p.expectChar(',')

	// Parse operator (string)
	p.skipWhitespace()
	operator, err := p.parseString()
	if err != nil {
		return nil, fmt.Errorf("domain_parse: operator: %w", err)
	}

	p.skipWhitespace()
	p.expectChar(',')

	// Parse value
	p.skipWhitespace()
	value, err := p.parseValue()
	if err != nil {
		return nil, fmt.Errorf("domain_parse: value for (%s, %s, ...): %w", field, operator, err)
	}

	p.skipWhitespace()
	if p.pos < len(p.input) && p.input[p.pos] == ')' {
		p.pos++ // consume ')'
	} else {
		return nil, fmt.Errorf("domain_parse: expected ')' at position %d", p.pos)
	}

	return Condition{Field: field, Operator: operator, Value: value}, nil
}

func (p *domainParser) parseValue() (Value, error) {
	p.skipWhitespace()
	if p.pos >= len(p.input) {
		return nil, fmt.Errorf("domain_parse: unexpected end of input in value")
	}

	ch := p.input[p.pos]

	// String literal
	if ch == '\'' || ch == '"' {
		return p.parseString()
	}

	// List literal
	if ch == '[' {
		return p.parseList()
	}

	// Tuple literal used as list value (some domain_force uses tuple syntax)
	if ch == '(' {
		return p.parseTupleAsList()
	}

	// Number or negative number
	if ch == '-' || (ch >= '0' && ch <= '9') {
		return p.parseNumber()
	}

	// Keywords or context variables
	if unicode.IsLetter(ch) || ch == '_' {
		return p.parseIdentOrKeyword()
	}

	return nil, fmt.Errorf("domain_parse: unexpected character %c at position %d in value", ch, p.pos)
}

func (p *domainParser) parseString() (string, error) {
	if p.pos >= len(p.input) {
		return "", fmt.Errorf("domain_parse: unexpected end of input in string")
	}

	quote := p.input[p.pos]
	if quote != '\'' && quote != '"' {
		return "", fmt.Errorf("domain_parse: expected quote at position %d, got %c", p.pos, quote)
	}
	p.pos++ // consume opening quote

	var sb strings.Builder
	for p.pos < len(p.input) {
		ch := p.input[p.pos]
		if ch == '\\' && p.pos+1 < len(p.input) {
			p.pos++
			sb.WriteRune(p.input[p.pos])
			p.pos++
			continue
		}
		if ch == quote {
			p.pos++ // consume closing quote
			return sb.String(), nil
		}
		sb.WriteRune(ch)
		p.pos++
	}

	return "", fmt.Errorf("domain_parse: unterminated string starting at position %d", p.pos)
}

func (p *domainParser) parseNumber() (Value, error) {
	start := p.pos
	if p.input[p.pos] == '-' {
		p.pos++
	}

	isFloat := false
	for p.pos < len(p.input) {
		ch := p.input[p.pos]
		if ch == '.' && !isFloat {
			isFloat = true
			p.pos++
			continue
		}
		if ch >= '0' && ch <= '9' {
			p.pos++
			continue
		}
		break
	}

	numStr := string(p.input[start:p.pos])

	if isFloat {
		f, err := strconv.ParseFloat(numStr, 64)
		if err != nil {
			return nil, fmt.Errorf("domain_parse: invalid float %q: %w", numStr, err)
		}
		return f, nil
	}

	n, err := strconv.ParseInt(numStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("domain_parse: invalid integer %q: %w", numStr, err)
	}
	return n, nil
}

func (p *domainParser) parseList() (Value, error) {
	if p.input[p.pos] != '[' {
		return nil, fmt.Errorf("domain_parse: expected '[' at position %d", p.pos)
	}
	p.pos++ // consume '['

	var items []interface{}
	for {
		p.skipWhitespace()
		if p.pos >= len(p.input) {
			return nil, fmt.Errorf("domain_parse: unterminated list")
		}
		if p.input[p.pos] == ']' {
			p.pos++ // consume ']'
			break
		}
		if p.input[p.pos] == ',' {
			p.pos++
			continue
		}

		val, err := p.parseValue()
		if err != nil {
			return nil, err
		}
		items = append(items, val)
	}

	// Try to produce typed slices for common cases.
	return normalizeListValue(items), nil
}

// parseTupleAsList parses a Python tuple literal (1, 2, 3) as a list value.
func (p *domainParser) parseTupleAsList() (Value, error) {
	if p.input[p.pos] != '(' {
		return nil, fmt.Errorf("domain_parse: expected '(' at position %d", p.pos)
	}
	p.pos++ // consume '('

	var items []interface{}
	for {
		p.skipWhitespace()
		if p.pos >= len(p.input) {
			return nil, fmt.Errorf("domain_parse: unterminated tuple-as-list")
		}
		if p.input[p.pos] == ')' {
			p.pos++ // consume ')'
			break
		}
		if p.input[p.pos] == ',' {
			p.pos++
			continue
		}

		val, err := p.parseValue()
		if err != nil {
			return nil, err
		}
		items = append(items, val)
	}

	return normalizeListValue(items), nil
}

// normalizeListValue converts []interface{} to typed slices when all elements
// share the same type, for compatibility with normalizeSlice in domain compilation.
func normalizeListValue(items []interface{}) interface{} {
	if len(items) == 0 {
		return []int64{}
	}

	// Check if all items are int64
	allInt := true
	for _, v := range items {
		if _, ok := v.(int64); !ok {
			allInt = false
			break
		}
	}
	if allInt {
		result := make([]int64, len(items))
		for i, v := range items {
			result[i] = v.(int64)
		}
		return result
	}

	// Check if all items are strings
	allStr := true
	for _, v := range items {
		if _, ok := v.(string); !ok {
			allStr = false
			break
		}
	}
	if allStr {
		result := make([]string, len(items))
		for i, v := range items {
			result[i] = v.(string)
		}
		return result
	}

	return items
}

func (p *domainParser) parseIdentOrKeyword() (Value, error) {
	start := p.pos
	for p.pos < len(p.input) {
		ch := p.input[p.pos]
		if unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '.' {
			p.pos++
		} else {
			break
		}
	}

	ident := string(p.input[start:p.pos])

	switch ident {
	case "True":
		return true, nil
	case "False":
		return false, nil
	case "None":
		return nil, nil

	// Context variables from _eval_context
	case "user.id":
		if p.env != nil {
			return p.env.UID(), nil
		}
		return int64(0), nil

	case "company_id", "user.company_id":
		if p.env != nil {
			return p.env.CompanyID(), nil
		}
		return int64(0), nil

	case "company_ids":
		if p.env != nil {
			return []int64{p.env.CompanyID()}, nil
		}
		return []int64{}, nil
	}

	// Handle dotted identifiers that start with known prefixes.
	// e.g., user.company_id.id, user.partner_id.id, etc.
	if strings.HasPrefix(ident, "user.") {
		// For now, resolve common patterns. Unknown paths return 0/nil.
		switch ident {
		case "user.company_id.id":
			if p.env != nil {
				return p.env.CompanyID(), nil
			}
			return int64(0), nil
		case "user.company_ids.ids":
			if p.env != nil {
				return []int64{p.env.CompanyID()}, nil
			}
			return []int64{}, nil
		default:
			// Unknown user attribute: return 0 as safe fallback.
			return int64(0), nil
		}
	}

	return nil, fmt.Errorf("domain_parse: unknown identifier %q at position %d", ident, start)
}

func (p *domainParser) skipWhitespace() {
	for p.pos < len(p.input) && unicode.IsSpace(p.input[p.pos]) {
		p.pos++
	}
}

func (p *domainParser) expectChar(ch rune) {
	p.skipWhitespace()
	if p.pos < len(p.input) && p.input[p.pos] == ch {
		p.pos++
	}
	// Tolerate missing comma (lenient parsing)
}
