package orm

import (
	"fmt"
	"strings"
)

// ProcessO2MCommands processes One2many field commands after a Create/Write.
// Mirrors: odoo/orm/fields_relational.py One2many write logic
//
// Commands:
//
//	CmdCreate(vals)     → Create child record with inverse_field = parentID
//	CmdUpdate(id, vals) → Update child record
//	CmdDelete(id)       → Delete child record
func ProcessO2MCommands(env *Environment, f *Field, parentID int64, cmds []Command) error {
	if f.Comodel == "" || f.InverseField == "" {
		return fmt.Errorf("orm: O2M field %q missing comodel or inverse_field", f.Name)
	}

	comodelRS := env.Model(f.Comodel)

	for _, cmd := range cmds {
		switch cmd.Operation {
		case CommandCreate:
			vals := cmd.Values
			if vals == nil {
				vals = make(Values)
			}
			vals[f.InverseField] = parentID
			if _, err := comodelRS.Create(vals); err != nil {
				return fmt.Errorf("orm: O2M create on %s: %w", f.Comodel, err)
			}

		case CommandUpdate:
			if cmd.ID <= 0 {
				continue
			}
			if err := comodelRS.Browse(cmd.ID).Write(cmd.Values); err != nil {
				return fmt.Errorf("orm: O2M update %s(%d): %w", f.Comodel, cmd.ID, err)
			}

		case CommandDelete:
			if cmd.ID <= 0 {
				continue
			}
			if err := comodelRS.Browse(cmd.ID).Unlink(); err != nil {
				return fmt.Errorf("orm: O2M delete %s(%d): %w", f.Comodel, cmd.ID, err)
			}

		case CommandClear:
			// Delete all children linked to this parent
			children, err := comodelRS.Search(And(Leaf(f.InverseField, "=", parentID)))
			if err != nil {
				return err
			}
			if err := children.Unlink(); err != nil {
				return err
			}
		}
	}
	return nil
}

// ProcessM2MCommands processes Many2many field commands after a Create/Write.
// Mirrors: odoo/orm/fields_relational.py Many2many write logic
//
// Commands:
//
//	CmdLink(id)    → Add link in junction table
//	CmdUnlink(id)  → Remove link from junction table
//	CmdClear()     → Remove all links
//	CmdSet(ids)    → Replace all links
//	CmdCreate(vals) → Create comodel record then link it
func ProcessM2MCommands(env *Environment, f *Field, parentID int64, cmds []Command) error {
	jt := f.JunctionTable()
	col1 := f.JunctionCol1()
	col2 := f.JunctionCol2()

	if jt == "" || col1 == "" || col2 == "" {
		return fmt.Errorf("orm: M2M field %q: cannot determine junction table", f.Name)
	}

	for _, cmd := range cmds {
		switch cmd.Operation {
		case CommandLink:
			_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
				`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
				jt, col1, col2,
			), parentID, cmd.ID)
			if err != nil {
				return fmt.Errorf("orm: M2M link %s: %w", f.Name, err)
			}

		case CommandUnlink:
			_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
				`DELETE FROM %q WHERE %q = $1 AND %q = $2`,
				jt, col1, col2,
			), parentID, cmd.ID)
			if err != nil {
				return fmt.Errorf("orm: M2M unlink %s: %w", f.Name, err)
			}

		case CommandClear:
			_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
				`DELETE FROM %q WHERE %q = $1`,
				jt, col1,
			), parentID)
			if err != nil {
				return fmt.Errorf("orm: M2M clear %s: %w", f.Name, err)
			}

		case CommandSet:
			// Clear then link all
			if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
				`DELETE FROM %q WHERE %q = $1`,
				jt, col1,
			), parentID); err != nil {
				return err
			}
			for _, targetID := range cmd.IDs {
				if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
					`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
					jt, col1, col2,
				), parentID, targetID); err != nil {
					return err
				}
			}

		case CommandCreate:
			// Create comodel record then link
			comodelRS := env.Model(f.Comodel)
			created, err := comodelRS.Create(cmd.Values)
			if err != nil {
				return err
			}
			if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
				`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
				jt, col1, col2,
			), parentID, created.ID()); err != nil {
				return err
			}
		}
	}
	return nil
}

// ReadM2MField reads the linked IDs for a Many2many field.
func ReadM2MField(env *Environment, f *Field, parentIDs []int64) (map[int64][]int64, error) {
	jt := f.JunctionTable()
	col1 := f.JunctionCol1()
	col2 := f.JunctionCol2()

	if jt == "" {
		return nil, nil
	}

	placeholders := make([]string, len(parentIDs))
	args := make([]interface{}, len(parentIDs))
	for i, id := range parentIDs {
		args[i] = id
		placeholders[i] = fmt.Sprintf("$%d", i+1)
	}

	query := fmt.Sprintf(
		`SELECT %q, %q FROM %q WHERE %q IN (%s)`,
		col1, col2, jt, col1, strings.Join(placeholders, ", "),
	)

	rows, err := env.tx.Query(env.ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	result := make(map[int64][]int64)
	for rows.Next() {
		var pid, tid int64
		if err := rows.Scan(&pid, &tid); err != nil {
			return nil, err
		}
		result[pid] = append(result[pid], tid)
	}
	return result, rows.Err()
}

// ParseCommands converts a JSON value to a slice of Commands.
// JSON format: [[0, 0, {vals}], [4, id, 0], [6, 0, [ids]], ...]
func ParseCommands(raw interface{}) ([]Command, bool) {
	list, ok := raw.([]interface{})
	if !ok || len(list) == 0 {
		return nil, false
	}

	// Check if this looks like commands (list of lists)
	first, ok := list[0].([]interface{})
	if !ok {
		return nil, false
	}
	if len(first) < 2 {
		return nil, false
	}

	var cmds []Command
	for _, item := range list {
		tuple, ok := item.([]interface{})
		if !ok || len(tuple) < 2 {
			continue
		}

		op, ok := toInt64(tuple[0])
		if !ok {
			continue
		}

		switch CommandOp(op) {
		case CommandCreate: // [0, _, {vals}]
			vals := make(Values)
			if len(tuple) > 2 {
				if m, ok := tuple[2].(map[string]interface{}); ok {
					vals = m
				}
			}
			cmds = append(cmds, CmdCreate(vals))

		case CommandUpdate: // [1, id, {vals}]
			id, _ := toInt64(tuple[1])
			vals := make(Values)
			if len(tuple) > 2 {
				if m, ok := tuple[2].(map[string]interface{}); ok {
					vals = m
				}
			}
			cmds = append(cmds, CmdUpdate(id, vals))

		case CommandDelete: // [2, id, _]
			id, _ := toInt64(tuple[1])
			cmds = append(cmds, CmdDelete(id))

		case CommandUnlink: // [3, id, _]
			id, _ := toInt64(tuple[1])
			cmds = append(cmds, CmdUnlink(id))

		case CommandLink: // [4, id, _]
			id, _ := toInt64(tuple[1])
			cmds = append(cmds, CmdLink(id))

		case CommandClear: // [5, _, _]
			cmds = append(cmds, CmdClear())

		case CommandSet: // [6, _, [ids]]
			var ids []int64
			if len(tuple) > 2 {
				if arr, ok := tuple[2].([]interface{}); ok {
					for _, v := range arr {
						if id, ok := toInt64(v); ok {
							ids = append(ids, id)
						}
					}
				}
			}
			cmds = append(cmds, CmdSet(ids))
		}
	}

	if len(cmds) == 0 {
		return nil, false
	}
	return cmds, true
}

// ToRecordID extracts an int64 ID from various numeric types. Public alias.
func ToRecordID(v interface{}) (int64, bool) { return toInt64(v) }

func toInt64(v interface{}) (int64, bool) {
	switch n := v.(type) {
	case float64:
		return int64(n), true
	case int64:
		return n, true
	case int32:
		return int64(n), true
	case int:
		return int64(n), true
	}
	return 0, false
}
