package server

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"odoo-go/pkg/orm"
)

// handleGetDefinitions returns field definitions for one or more models.
// Mirrors: odoo/addons/web/controllers/model.py Model.get_definitions()
func (s *Server) handleGetDefinitions(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	var req JSONRPCRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		s.writeJSONRPC(w, nil, nil, &RPCError{Code: -32700, Message: "Parse error"})
		return
	}
	var params struct {
		Models []string `json:"model_names"`
	}
	json.Unmarshal(req.Params, &params)

	result := make(map[string]interface{})
	for _, modelName := range params.Models {
		result[modelName] = fieldsGetForModel(modelName)
	}
	s.writeJSONRPC(w, req.ID, result, nil)
}

// handleGetViews implements the get_views method.
// Mirrors: odoo/addons/base/models/ir_ui_view.py get_views()
func handleGetViews(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) {
	// Parse views list: [[false, "list"], [false, "form"], [false, "search"]]
	var viewRequests [][]interface{}
	if len(params.Args) > 0 {
		if vr, ok := params.Args[0].([]interface{}); ok {
			viewRequests = make([][]interface{}, len(vr))
			for i, v := range vr {
				if pair, ok := v.([]interface{}); ok {
					viewRequests[i] = pair
				}
			}
		}
	}
	// Also check kwargs
	if viewRequests == nil {
		if vr, ok := params.KW["views"].([]interface{}); ok {
			viewRequests = make([][]interface{}, len(vr))
			for i, v := range vr {
				if pair, ok := v.([]interface{}); ok {
					viewRequests[i] = pair
				}
			}
		}
	}

	views := make(map[string]interface{})
	for _, vr := range viewRequests {
		if len(vr) < 2 {
			continue
		}
		viewType, _ := vr[1].(string)
		if viewType == "" {
			continue
		}

		// Try to load from ir_ui_view table
		arch := loadViewArch(env, model, viewType)
		if arch == "" {
			// Generate default view
			arch = generateDefaultView(model, viewType)
		}

		views[viewType] = map[string]interface{}{
			"arch":         arch,
			"type":         viewType,
			"model":        model,
			"view_id":      0,
			"field_parent":  false,
		}
	}

	// Always include search view (client expects it)
	if _, hasSearch := views["search"]; !hasSearch {
		arch := loadViewArch(env, model, "search")
		if arch == "" {
			arch = generateDefaultView(model, "search")
		}
		views["search"] = map[string]interface{}{
			"arch":         arch,
			"type":         "search",
			"model":        model,
			"view_id":      0,
			"field_parent":  false,
		}
	}

	// Build models dict with field metadata.
	// Include the main model + all comodels referenced by relational fields.
	// Mirrors: odoo/addons/web/models/models.py _get_view_fields()
	models := map[string]interface{}{
		model: map[string]interface{}{
			"fields": fieldsGetForModel(model),
		},
	}

	// Add comodels (needed for O2M/M2M inline views and M2O dropdowns)
	m := orm.Registry.Get(model)
	if m != nil {
		for _, f := range m.Fields() {
			if f.Comodel != "" {
				if _, exists := models[f.Comodel]; !exists {
					comodelFields := fieldsGetForModel(f.Comodel)
					if len(comodelFields) > 0 {
						models[f.Comodel] = map[string]interface{}{
							"fields": comodelFields,
						}
					}
				}
			}
		}
	}

	return map[string]interface{}{
		"views":  views,
		"models": models,
	}, nil
}

// loadViewArch tries to load a view from the ir_ui_view table.
// After loading the base view, it queries for inheriting views and merges them.
// Mirrors: odoo/addons/base/models/ir_ui_view.py _get_combined_arch()
func loadViewArch(env *orm.Environment, model, viewType string) string {
	// Load base view (no parent — inherit_id IS NULL)
	var arch string
	var viewID int64
	err := env.Tx().QueryRow(env.Ctx(),
		`SELECT id, arch FROM ir_ui_view
		 WHERE model = $1 AND type = $2 AND active = true AND inherit_id IS NULL
		 ORDER BY priority LIMIT 1`,
		model, viewType,
	).Scan(&viewID, &arch)
	if err != nil {
		return ""
	}

	// Load inheriting views and apply them in priority order
	rows, err := env.Tx().Query(env.Ctx(),
		`SELECT arch FROM ir_ui_view
		 WHERE inherit_id = $1 AND active = true
		 ORDER BY priority`,
		viewID,
	)
	if err != nil {
		return arch
	}
	defer rows.Close()

	for rows.Next() {
		var inheritArch string
		if err := rows.Scan(&inheritArch); err != nil {
			continue
		}
		arch = applyViewInheritance(arch, inheritArch)
	}

	return arch
}

// generateDefaultView creates a minimal view XML for a model.
func generateDefaultView(modelName, viewType string) string {
	m := orm.Registry.Get(modelName)
	if m == nil {
		return fmt.Sprintf("<%s><field name=\"id\"/></%s>", viewType, viewType)
	}

	switch viewType {
	case "list", "tree":
		return generateDefaultListView(m)
	case "form":
		return generateDefaultFormView(m)
	case "search":
		return generateDefaultSearchView(m)
	case "kanban":
		return generateDefaultKanbanView(m)
	case "pivot":
		return generateDefaultPivotView(m)
	case "graph":
		return generateDefaultGraphView(m)
	default:
		return fmt.Sprintf("<%s><field name=\"id\"/></%s>", viewType, viewType)
	}
}

func generateDefaultListView(m *orm.Model) string {
	// Prioritize important fields first
	priority := []string{"name", "display_name", "state", "partner_id", "date_order", "date",
		"amount_total", "amount_untaxed", "email", "phone", "company_id", "user_id",
		"product_id", "quantity", "price_unit", "price_subtotal"}

	var fields []string
	added := make(map[string]bool)

	// Add priority fields first
	for _, pf := range priority {
		f := m.GetField(pf)
		if f != nil && f.IsStored() && f.Type != orm.TypeBinary {
			fields = append(fields, fmt.Sprintf(`<field name="%s"/>`, pf))
			added[pf] = true
		}
	}

	// Fill remaining slots
	for _, f := range m.Fields() {
		if len(fields) >= 10 {
			break
		}
		if added[f.Name] || f.Name == "id" || !f.IsStored() ||
			f.Name == "create_uid" || f.Name == "write_uid" ||
			f.Name == "create_date" || f.Name == "write_date" ||
			f.Name == "password" || f.Name == "password_crypt" ||
			f.Type == orm.TypeBinary || f.Type == orm.TypeText || f.Type == orm.TypeHTML {
			continue
		}
		fields = append(fields, fmt.Sprintf(`<field name="%s"/>`, f.Name))
		added[f.Name] = true
	}

	return fmt.Sprintf("<list>\n  %s\n</list>", strings.Join(fields, "\n  "))
}

func generateDefaultFormView(m *orm.Model) string {
	skip := map[string]bool{
		"id": true, "create_uid": true, "write_uid": true,
		"create_date": true, "write_date": true,
		"password": true, "password_crypt": true,
	}

	// Header with action buttons and state widget
	// Mirrors: odoo form views with <header><button .../><field name="state" widget="statusbar"/></header>
	var header string
	if f := m.GetField("state"); f != nil && f.Type == orm.TypeSelection {
		var buttons []string
		// Generate buttons from registered methods that look like actions
		if m.Methods != nil {
			actionMethods := []struct{ method, label, stateFilter string }{
				{"action_confirm", "Confirm", "draft"},
				{"action_post", "Post", "draft"},
				{"action_done", "Done", "confirmed"},
				{"action_cancel", "Cancel", ""},
				{"button_cancel", "Cancel", ""},
				{"button_draft", "Reset to Draft", "cancel"},
				{"action_send", "Send", "posted"},
				{"create_invoices", "Create Invoice", "sale"},
			}
			for _, am := range actionMethods {
				if _, ok := m.Methods[am.method]; ok {
					attrs := ""
					if am.stateFilter != "" {
						attrs = fmt.Sprintf(` invisible="state != '%s'"`, am.stateFilter)
					}
					btnClass := "btn-secondary"
					if am.method == "action_confirm" || am.method == "action_post" {
						btnClass = "btn-primary"
					}
					buttons = append(buttons, fmt.Sprintf(
						`    <button name="%s" string="%s" type="object" class="%s"%s/>`,
						am.method, am.label, btnClass, attrs))
				}
			}
		}
		header = "  <header>\n"
		for _, btn := range buttons {
			header += btn + "\n"
		}
		header += `    <field name="state" widget="statusbar" clickable="1"/>
  </header>
`
	}

	// Title field (name or display_name)
	var title string
	if f := m.GetField("name"); f != nil {
		title = `    <div class="oe_title">
      <h1><field name="name" placeholder="Name..."/></h1>
    </div>
`
		skip["name"] = true
	}

	// Split fields into left/right groups
	var leftFields, rightFields []string
	var o2mFields []string
	count := 0

	// Prioritize important fields
	priority := []string{"partner_id", "date_order", "date", "company_id", "currency_id",
		"user_id", "journal_id", "product_id", "email", "phone"}

	for _, pf := range priority {
		f := m.GetField(pf)
		if f == nil || skip[pf] || f.Type == orm.TypeBinary {
			continue
		}
		skip[pf] = true
		line := fmt.Sprintf(`        <field name="%s"/>`, pf)
		if count%2 == 0 {
			leftFields = append(leftFields, line)
		} else {
			rightFields = append(rightFields, line)
		}
		count++
	}

	// Add remaining stored fields
	for _, f := range m.Fields() {
		if skip[f.Name] || !f.IsStored() || f.Type == orm.TypeBinary {
			continue
		}
		if f.Type == orm.TypeOne2many {
			o2mFields = append(o2mFields, fmt.Sprintf(`    <field name="%s"/>`, f.Name))
			continue
		}
		if f.Type == orm.TypeMany2many {
			continue
		}
		line := fmt.Sprintf(`        <field name="%s"/>`, f.Name)
		if len(leftFields) <= len(rightFields) {
			leftFields = append(leftFields, line)
		} else {
			rightFields = append(rightFields, line)
		}
		if len(leftFields)+len(rightFields) >= 20 {
			break
		}
	}

	// Build form
	var buf strings.Builder
	buf.WriteString("<form>\n")
	buf.WriteString(header)
	buf.WriteString("  <sheet>\n")
	buf.WriteString(title)
	buf.WriteString("    <group>\n")
	buf.WriteString("      <group>\n")
	buf.WriteString(strings.Join(leftFields, "\n"))
	buf.WriteString("\n      </group>\n")
	buf.WriteString("      <group>\n")
	buf.WriteString(strings.Join(rightFields, "\n"))
	buf.WriteString("\n      </group>\n")
	buf.WriteString("    </group>\n")

	// O2M fields in notebook
	if len(o2mFields) > 0 {
		buf.WriteString("    <notebook>\n")
		buf.WriteString("      <page string=\"Lines\">\n")
		buf.WriteString(strings.Join(o2mFields, "\n"))
		buf.WriteString("\n      </page>\n")
		buf.WriteString("    </notebook>\n")
	}

	buf.WriteString("  </sheet>\n")
	buf.WriteString("</form>")
	return buf.String()
}

func generateDefaultSearchView(m *orm.Model) string {
	var fields []string
	var filters []string

	// Search fields
	searchable := []string{"name", "display_name", "email", "phone", "ref",
		"partner_id", "company_id", "user_id", "state", "date_order", "date"}
	for _, sf := range searchable {
		if f := m.GetField(sf); f != nil {
			fields = append(fields, fmt.Sprintf(`<field name="%s"/>`, sf))
		}
	}
	if len(fields) == 0 {
		fields = append(fields, `<field name="id"/>`)
	}

	// Auto-generate filter for state field
	if f := m.GetField("state"); f != nil && f.Type == orm.TypeSelection {
		for _, sel := range f.Selection {
			filters = append(filters, fmt.Sprintf(
				`<filter string="%s" name="filter_%s" domain="[('state','=','%s')]"/>`,
				sel.Label, sel.Value, sel.Value))
		}
	}

	// Group-by for common fields
	var groupby []string
	groupable := []string{"partner_id", "state", "company_id", "user_id", "stage_id"}
	for _, gf := range groupable {
		if f := m.GetField(gf); f != nil {
			groupby = append(groupby, fmt.Sprintf(`<filter string="%s" name="groupby_%s" context="{'group_by': '%s'}"/>`,
				f.String, gf, gf))
		}
	}

	var buf strings.Builder
	buf.WriteString("<search>\n")
	for _, f := range fields {
		buf.WriteString("  " + f + "\n")
	}
	if len(filters) > 0 {
		buf.WriteString("  <separator/>\n")
		for _, f := range filters {
			buf.WriteString("  " + f + "\n")
		}
	}
	if len(groupby) > 0 {
		buf.WriteString("  <group expand=\"0\" string=\"Group By\">\n")
		for _, g := range groupby {
			buf.WriteString("    " + g + "\n")
		}
		buf.WriteString("  </group>\n")
	}
	buf.WriteString("</search>")
	return buf.String()
}

func generateDefaultKanbanView(m *orm.Model) string {
	nameField := "name"
	if f := m.GetField("name"); f == nil {
		nameField = "id"
	}

	// Build a richer card with available fields
	var cardFields []string

	// Title
	cardFields = append(cardFields, fmt.Sprintf(`      <field name="%s" class="fw-bold fs-5"/>`, nameField))

	// Partner/customer
	if f := m.GetField("partner_id"); f != nil {
		cardFields = append(cardFields, `      <field name="partner_id"/>`)
	}

	// Revenue/amount
	for _, amtField := range []string{"expected_revenue", "amount_total", "amount_untaxed"} {
		if f := m.GetField(amtField); f != nil {
			cardFields = append(cardFields, fmt.Sprintf(`      <field name="%s"/>`, amtField))
			break
		}
	}

	// Date
	for _, dateField := range []string{"date_order", "date", "date_deadline"} {
		if f := m.GetField(dateField); f != nil {
			cardFields = append(cardFields, fmt.Sprintf(`      <field name="%s"/>`, dateField))
			break
		}
	}

	// User/assignee
	if f := m.GetField("user_id"); f != nil {
		cardFields = append(cardFields, `      <field name="user_id" widget="many2one_avatar_user"/>`)
	}

	return fmt.Sprintf(`<kanban>
  <templates>
    <t t-name="card">
%s
    </t>
  </templates>
</kanban>`, strings.Join(cardFields, "\n"))
}

// generateDefaultPivotView creates a minimal pivot view with numeric measure fields.
// Mirrors: odoo/addons/web/models/ir_ui_view.py default_view()
func generateDefaultPivotView(m *orm.Model) string {
	var measures []string
	for _, name := range sortedFieldNames(m) {
		f := m.GetField(name)
		if f == nil {
			continue
		}
		if (f.Type == orm.TypeFloat || f.Type == orm.TypeInteger || f.Type == orm.TypeMonetary) && f.IsStored() && f.Name != "id" {
			measures = append(measures, fmt.Sprintf(`<field name="%s" type="measure"/>`, f.Name))
			if len(measures) >= 3 {
				break
			}
		}
	}
	if len(measures) == 0 {
		measures = append(measures, `<field name="id" type="measure"/>`)
	}
	return fmt.Sprintf("<pivot>\n  %s\n</pivot>", strings.Join(measures, "\n  "))
}

// generateDefaultGraphView creates a minimal graph view with a dimension and a measure.
// Mirrors: odoo/addons/web/models/ir_ui_view.py default_view()
func generateDefaultGraphView(m *orm.Model) string {
	var fields []string
	// First groupable field as dimension
	for _, name := range sortedFieldNames(m) {
		f := m.GetField(name)
		if f == nil {
			continue
		}
		if f.IsStored() && (f.Type == orm.TypeMany2one || f.Type == orm.TypeSelection) {
			fields = append(fields, fmt.Sprintf(`<field name="%s"/>`, f.Name))
			break
		}
	}
	// First numeric field as measure
	for _, name := range sortedFieldNames(m) {
		f := m.GetField(name)
		if f == nil {
			continue
		}
		if (f.Type == orm.TypeFloat || f.Type == orm.TypeInteger || f.Type == orm.TypeMonetary) && f.IsStored() && f.Name != "id" {
			fields = append(fields, fmt.Sprintf(`<field name="%s" type="measure"/>`, f.Name))
			break
		}
	}
	if len(fields) == 0 {
		fields = append(fields, `<field name="id" type="measure"/>`)
	}
	return fmt.Sprintf("<graph>\n  %s\n</graph>", strings.Join(fields, "\n  "))
}

// sortedFieldNames returns field names in alphabetical order for deterministic output.
func sortedFieldNames(m *orm.Model) []string {
	fields := m.Fields()
	names := make([]string, 0, len(fields))
	for name := range fields {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

// --- View Inheritance ---

// applyViewInheritance merges an inheriting view's arch into a base view.
// Supports:
//   - <xpath expr="//field[@name='X']" position="after|before|replace|inside">
//   - <field name="X" position="after|before|replace|inside"> (shorthand)
//
// Mirrors (simplified): odoo/addons/base/models/ir_ui_view.py apply_inheritance_specs()
func applyViewInheritance(baseArch, inheritArch string) string {
	directives := parseInheritDirectives(inheritArch)
	if len(directives) == 0 {
		return baseArch
	}

	for _, d := range directives {
		baseArch = applyDirective(baseArch, d)
	}
	return baseArch
}

// inheritDirective represents a single modification instruction from an inheriting view.
type inheritDirective struct {
	// target identifies what to match: tag name + attribute match
	targetTag  string // e.g., "field", "group", "page", "notebook", etc.
	targetAttr string // attribute name to match, e.g., "name"
	targetVal  string // attribute value to match, e.g., "partner_id"
	position   string // "after", "before", "replace", "inside", "attributes"
	content    string // inner XML content to insert/replace
}

// parseInheritDirectives extracts modification directives from an inheriting view arch.
func parseInheritDirectives(inheritArch string) []inheritDirective {
	// Wrap in a root element so the XML decoder can parse fragments
	wrapped := "<_root>" + strings.TrimSpace(inheritArch) + "</_root>"

	decoder := xml.NewDecoder(strings.NewReader(wrapped))
	var directives []inheritDirective

	// We need to find the top-level children of _root.
	// Skip the outer <data> or root element if present.
	// The actual directives are either <xpath> or <field> etc. with position attr.

	type element struct {
		name  xml.Name
		attrs []xml.Attr
	}

	depth := 0
	var stack []element
	var contentBuilder strings.Builder
	captureDepth := -1

	for {
		tok, err := decoder.Token()
		if err != nil {
			break
		}

		switch t := tok.(type) {
		case xml.StartElement:
			depth++

			// If we're capturing content for a directive, write this element
			if captureDepth >= 0 && depth > captureDepth+1 {
				contentBuilder.WriteString(startElementToString(t))
			}

			stack = append(stack, element{name: t.Name, attrs: t.Attr})

			// Skip the wrapper _root and optional <data> root element
			if depth == 1 && t.Name.Local == "_root" {
				continue
			}
			if depth == 2 && t.Name.Local == "data" {
				continue
			}

			// Check if this is a directive element (xpath or element with position)
			pos := attrVal(t.Attr, "position")
			if pos != "" && captureDepth < 0 {
				d := inheritDirective{position: pos}

				if t.Name.Local == "xpath" {
					// Parse expr attribute: //field[@name='partner_id']
					expr := attrVal(t.Attr, "expr")
					d.targetTag, d.targetAttr, d.targetVal = parseXPathExpr(expr)
				} else {
					// Shorthand: <field name="X" position="after">
					d.targetTag = t.Name.Local
					d.targetAttr = "name"
					d.targetVal = attrVal(t.Attr, "name")
				}

				if d.targetTag != "" {
					directives = append(directives, d)
					captureDepth = depth
					contentBuilder.Reset()
				}
			}

		case xml.EndElement:
			if captureDepth >= 0 {
				if depth == captureDepth {
					// We've closed the directive element, store captured content
					if len(directives) > 0 {
						directives[len(directives)-1].content = strings.TrimSpace(contentBuilder.String())
					}
					captureDepth = -1
					contentBuilder.Reset()
				} else if depth > captureDepth {
					contentBuilder.WriteString(fmt.Sprintf("</%s>", t.Name.Local))
				}
			}

			depth--
			if len(stack) > 0 {
				stack = stack[:len(stack)-1]
			}

		case xml.CharData:
			if captureDepth >= 0 && depth > captureDepth {
				contentBuilder.Write(t)
			}
		}
	}

	return directives
}

// startElementToString serializes an xml.StartElement back to string form.
func startElementToString(el xml.StartElement) string {
	var buf strings.Builder
	buf.WriteString("<")
	buf.WriteString(el.Name.Local)
	for _, a := range el.Attr {
		buf.WriteString(fmt.Sprintf(` %s="%s"`, a.Name.Local, a.Value))
	}
	buf.WriteString(">")
	return buf.String()
}

// attrVal returns the value of a named attribute, or "".
func attrVal(attrs []xml.Attr, name string) string {
	for _, a := range attrs {
		if a.Name.Local == name {
			return a.Value
		}
	}
	return ""
}

// parseXPathExpr parses a simple XPath expression like //field[@name='partner_id'].
// Returns (tag, attrName, attrValue). Only supports the //tag[@attr='val'] pattern.
func parseXPathExpr(expr string) (string, string, string) {
	// Pattern: //tag[@attr='val'] or //tag[@attr="val"]
	expr = strings.TrimSpace(expr)

	// Strip leading //
	expr = strings.TrimPrefix(expr, "//")

	// Find tag[
	bracketIdx := strings.Index(expr, "[")
	if bracketIdx < 0 {
		// Simple case: //tag — match by tag name alone
		tag := strings.TrimSpace(expr)
		if tag != "" {
			return tag, "", ""
		}
		return "", "", ""
	}

	tag := expr[:bracketIdx]

	// Extract @attr='val'
	inner := expr[bracketIdx+1:]
	inner = strings.TrimSuffix(inner, "]")
	inner = strings.TrimPrefix(inner, "@")

	eqIdx := strings.Index(inner, "=")
	if eqIdx < 0 {
		return tag, "", ""
	}

	attrName := inner[:eqIdx]
	attrValue := inner[eqIdx+1:]
	// Remove quotes
	attrValue = strings.Trim(attrValue, "'\"")

	return tag, attrName, attrValue
}

// applyDirective applies a single inheritance directive to the base arch.
func applyDirective(baseArch string, d inheritDirective) string {
	// Find the target element in baseArch
	targetStart, targetEnd := findElement(baseArch, d.targetTag, d.targetAttr, d.targetVal)
	if targetStart < 0 {
		return baseArch
	}

	switch d.position {
	case "after":
		// Insert content after the target element
		return baseArch[:targetEnd] + "\n" + d.content + baseArch[targetEnd:]

	case "before":
		// Insert content before the target element
		return baseArch[:targetStart] + d.content + "\n" + baseArch[targetStart:]

	case "replace":
		// Replace the target element with content
		if d.content == "" {
			// Empty replace = remove the element
			return baseArch[:targetStart] + baseArch[targetEnd:]
		}
		return baseArch[:targetStart] + d.content + baseArch[targetEnd:]

	case "inside":
		// Insert content inside the target element (before closing tag)
		// Need to find the closing tag of the target
		closingTag := fmt.Sprintf("</%s>", d.targetTag)
		closeIdx := strings.LastIndex(baseArch[targetStart:targetEnd], closingTag)
		if closeIdx >= 0 {
			insertPos := targetStart + closeIdx
			return baseArch[:insertPos] + "\n" + d.content + "\n" + baseArch[insertPos:]
		}
		// Self-closing element — convert to open/close with content
		selfClose := strings.LastIndex(baseArch[targetStart:targetEnd], "/>")
		if selfClose >= 0 {
			insertPos := targetStart + selfClose
			return baseArch[:insertPos] + ">\n" + d.content + "\n" + fmt.Sprintf("</%s>", d.targetTag) + baseArch[insertPos+2:]
		}
		return baseArch

	case "attributes":
		// Modify attributes of the target element — not implemented yet
		return baseArch

	default:
		return baseArch
	}
}

// findElement finds a target element in the XML string by tag name and optional attribute match.
// Returns (startIndex, endIndex) of the full element (including closing tag if present).
// Returns (-1, -1) if not found.
func findElement(xmlStr, tag, attrName, attrVal string) (int, int) {
	searchFrom := 0
	for searchFrom < len(xmlStr) {
		// Find next occurrence of <tag
		tagOpen := "<" + tag
		idx := strings.Index(xmlStr[searchFrom:], tagOpen)
		if idx < 0 {
			return -1, -1
		}
		idx += searchFrom

		// Ensure this is actually a tag start (not e.g. <fields inside <fieldset)
		afterTag := idx + len(tagOpen)
		if afterTag < len(xmlStr) {
			ch := xmlStr[afterTag]
			if ch != ' ' && ch != '/' && ch != '>' && ch != '\n' && ch != '\t' && ch != '\r' {
				searchFrom = afterTag
				continue
			}
		}

		// Find end of opening tag
		openEnd := strings.Index(xmlStr[idx:], ">")
		if openEnd < 0 {
			return -1, -1
		}
		openEnd += idx

		openingTag := xmlStr[idx : openEnd+1]

		// Check attribute match if required
		if attrName != "" && attrVal != "" {
			if !matchAttribute(openingTag, attrName, attrVal) {
				searchFrom = openEnd + 1
				continue
			}
		}

		// Check if self-closing
		if strings.HasSuffix(strings.TrimSpace(openingTag), "/>") {
			return idx, openEnd + 1
		}

		// Find matching closing tag
		closeTag := fmt.Sprintf("</%s>", tag)
		// Simple approach: find the next closing tag at the same nesting level
		closeIdx := findMatchingClose(xmlStr[openEnd+1:], tag)
		if closeIdx < 0 {
			// No closing tag — treat as self-contained up to >
			return idx, openEnd + 1
		}
		endIdx := openEnd + 1 + closeIdx + len(closeTag)
		return idx, endIdx
	}
	return -1, -1
}

// findMatchingClose finds the matching closing tag, handling nesting.
// Returns the offset from the start of s where the closing tag begins,
// or -1 if not found.
func findMatchingClose(s, tag string) int {
	openTag := "<" + tag
	closeTag := "</" + tag + ">"
	depth := 1
	pos := 0

	for pos < len(s) {
		nextOpen := strings.Index(s[pos:], openTag)
		nextClose := strings.Index(s[pos:], closeTag)

		if nextClose < 0 {
			return -1
		}

		// If there's a nested open before the close, increase depth
		if nextOpen >= 0 && nextOpen < nextClose {
			// Verify it's actually a tag start (not a prefix of another tag)
			afterIdx := pos + nextOpen + len(openTag)
			if afterIdx < len(s) {
				ch := s[afterIdx]
				if ch == ' ' || ch == '/' || ch == '>' || ch == '\n' || ch == '\t' || ch == '\r' {
					depth++
				}
			}
			pos += nextOpen + len(openTag)
			continue
		}

		depth--
		if depth == 0 {
			return pos + nextClose
		}
		pos += nextClose + len(closeTag)
	}
	return -1
}

// matchAttribute checks if an opening tag string contains attr="val" or attr='val'.
func matchAttribute(openingTag, attrName, attrValue string) bool {
	// Try both quote styles
	pattern1 := fmt.Sprintf(`%s="%s"`, attrName, attrValue)
	pattern2 := fmt.Sprintf(`%s='%s'`, attrName, attrValue)
	return strings.Contains(openingTag, pattern1) || strings.Contains(openingTag, pattern2)
}
