package browser

import (
	"fmt"
	"strings"

	"github.com/go-rod/rod/lib/proto"
)

// axValue extracts a string value from an AXValue.
// Ported from TS cdp.ts:170-190 (axValue function).
func axValue(v *proto.AccessibilityAXValue) string {
	if v == nil {
		return ""
	}
	// gson.JSON — call Str() for string, or String() for raw JSON
	s := v.Value.Str()
	if s != "" {
		return s
	}
	// Try raw string representation for numbers/booleans
	raw := v.Value.String()
	if raw == "" || raw == "null" || raw == "\"\"" {
		return ""
	}
	return raw
}

// axNodeTree is an internal tree node built from flat AX nodes.
type axNodeTree struct {
	node     *proto.AccessibilityAXNode
	depth    int
}

// buildAXTree converts flat CDP AX nodes into a tree structure.
// Ported from TS cdp.ts:192-249 (formatAriaSnapshot).
func buildAXTree(nodes []*proto.AccessibilityAXNode, limit int) []*axNodeTree {
	if len(nodes) == 0 {
		return nil
	}

	byID := make(map[proto.AccessibilityAXNodeID]*proto.AccessibilityAXNode, len(nodes))
	for _, n := range nodes {
		if n.NodeID != "" {
			byID[n.NodeID] = n
		}
	}

	// Find root: a node not referenced as a child by any other node
	referenced := make(map[proto.AccessibilityAXNodeID]bool)
	for _, n := range nodes {
		for _, cid := range n.ChildIDs {
			referenced[cid] = true
		}
	}

	var root *proto.AccessibilityAXNode
	for _, n := range nodes {
		if n.NodeID != "" && !referenced[n.NodeID] {
			root = n
			break
		}
	}
	if root == nil && len(nodes) > 0 {
		root = nodes[0]
	}
	if root == nil || root.NodeID == "" {
		return nil
	}

	// DFS traversal using explicit stack (matching TS behavior)
	var result []*axNodeTree
	type stackItem struct {
		id    proto.AccessibilityAXNodeID
		depth int
	}
	stack := []stackItem{{id: root.NodeID, depth: 0}}

	for len(stack) > 0 && len(result) < limit {
		item := stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		n, ok := byID[item.id]
		if !ok {
			continue
		}

		treeNode := &axNodeTree{
			node:  n,
			depth: item.depth,
		}
		result = append(result, treeNode)

		// Push children in reverse order so first child is processed first
		children := n.ChildIDs
		for i := len(children) - 1; i >= 0; i-- {
			cid := children[i]
			if _, exists := byID[cid]; exists {
				stack = append(stack, stackItem{id: cid, depth: item.depth + 1})
			}
		}
	}

	return result
}

// roleNameTracker tracks role+name combinations for nth deduplication.
// Ported from TS pw-role-snapshot.ts:129-170.
type roleNameTracker struct {
	counts    map[string]int
	refsByKey map[string][]string
}

func newRoleNameTracker() *roleNameTracker {
	return &roleNameTracker{
		counts:    make(map[string]int),
		refsByKey: make(map[string][]string),
	}
}

func (t *roleNameTracker) key(role, name string) string {
	return role + ":" + name
}

func (t *roleNameTracker) getNextIndex(role, name string) int {
	k := t.key(role, name)
	idx := t.counts[k]
	t.counts[k] = idx + 1
	return idx
}

func (t *roleNameTracker) trackRef(role, name, ref string) {
	k := t.key(role, name)
	t.refsByKey[k] = append(t.refsByKey[k], ref)
}

func (t *roleNameTracker) getDuplicateKeys() map[string]bool {
	dups := make(map[string]bool)
	for k, refs := range t.refsByKey {
		if len(refs) > 1 {
			dups[k] = true
		}
	}
	return dups
}

// removeNthFromNonDuplicates cleans up nth=0 from refs that have no duplicates.
func removeNthFromNonDuplicates(refs map[string]RoleRef, tracker *roleNameTracker) {
	dups := tracker.getDuplicateKeys()
	for ref, data := range refs {
		k := tracker.key(data.Role, data.Name)
		if !dups[k] {
			data.Nth = 0
			refs[ref] = data
		}
	}
}

// FormatSnapshot converts raw CDP AX nodes into a text tree with refs.
// This is the core algorithm combining:
// - TS cdp.ts:192-249 (formatAriaSnapshot) — tree building
// - TS pw-role-snapshot.ts:207-267 (processLine) — role filtering + ref assignment
func FormatSnapshot(nodes []*proto.AccessibilityAXNode, opts SnapshotOptions) *SnapshotResult {
	if opts.MaxChars == 0 {
		opts.MaxChars = 8000
	}
	if opts.Limit == 0 {
		opts.Limit = 500
	}

	treeNodes := buildAXTree(nodes, opts.Limit)
	if len(treeNodes) == 0 {
		return &SnapshotResult{
			Snapshot: "(empty page)",
			Refs:     map[string]RoleRef{},
			Stats:    SnapshotStats{Lines: 1, Chars: 12},
		}
	}

	refs := make(map[string]RoleRef)
	tracker := newRoleNameTracker()
	refCounter := 0
	nextRef := func() string {
		refCounter++
		return fmt.Sprintf("e%d", refCounter)
	}

	var lines []string
	interactiveCount := 0

	for _, tn := range treeNodes {
		role := strings.ToLower(axValue(tn.node.Role))
		name := axValue(tn.node.Name)
		value := axValue(tn.node.Value)
		description := axValue(tn.node.Description)

		// Skip empty/invisible roles
		if role == "" || role == "none" || role == "unknown" {
			if name == "" {
				continue
			}
		}

		// Skip low-value leaf roles that clutter the tree.
		// statictext and inlinetextbox are internal text representation nodes.
		if role == "statictext" || role == "inlinetextbox" {
			continue
		}

		// Apply depth filter
		if opts.MaxDepth > 0 && tn.depth > opts.MaxDepth {
			continue
		}

		isInteractive := IsInteractive(role)
		isContent := IsContent(role)
		isStruct := IsStructural(role)

		// Interactive-only mode: skip non-interactive
		if opts.Interactive && !isInteractive {
			continue
		}

		// Compact mode: skip unnamed structural elements
		if opts.Compact && isStruct && name == "" {
			continue
		}

		// Build the line
		indent := strings.Repeat("  ", tn.depth)
		line := indent + "- " + role

		if name != "" {
			line += fmt.Sprintf(" %q", name)
		}

		// Determine if this element should get a ref
		shouldHaveRef := isInteractive || (isContent && name != "")
		if shouldHaveRef {
			ref := nextRef()
			nth := tracker.getNextIndex(role, name)
			tracker.trackRef(role, name, ref)

			backendNodeID := int(tn.node.BackendDOMNodeID)

			refs[ref] = RoleRef{
				Role:          role,
				Name:          name,
				Nth:           nth,
				BackendNodeID: backendNodeID,
			}

			line += fmt.Sprintf(" [ref=%s]", ref)
			if nth > 0 {
				line += fmt.Sprintf(" [nth=%d]", nth)
			}

			if isInteractive {
				interactiveCount++
			}
		}

		// Append value/description if present
		if value != "" {
			line += fmt.Sprintf(": %q", value)
		}
		if description != "" {
			line += fmt.Sprintf(" (%s)", description)
		}

		lines = append(lines, line)
	}

	// Remove nth from non-duplicate refs
	removeNthFromNonDuplicates(refs, tracker)

	snapshot := strings.Join(lines, "\n")
	if len(lines) == 0 {
		snapshot = "(empty page)"
	}

	// Compact mode: remove structural lines that have no ref descendants
	if opts.Compact && len(lines) > 0 {
		snapshot = compactTree(snapshot)
	}

	// Truncate if needed
	truncated := false
	if opts.MaxChars > 0 && len(snapshot) > opts.MaxChars {
		snapshot = snapshot[:opts.MaxChars] + "\n[...TRUNCATED]"
		truncated = true
	}

	return &SnapshotResult{
		Snapshot:  snapshot,
		Refs:      refs,
		Truncated: truncated,
		Stats: SnapshotStats{
			Lines:       len(lines),
			Chars:       len(snapshot),
			Refs:        len(refs),
			Interactive: interactiveCount,
		},
	}
}

// compactTree removes structural lines that have no ref descendants.
// Ported from TS pw-role-snapshot.ts:172-205.
func compactTree(tree string) string {
	lines := strings.Split(tree, "\n")
	var result []string

	for i, line := range lines {
		// Keep lines with refs
		if strings.Contains(line, "[ref=") {
			result = append(result, line)
			continue
		}
		// Keep lines with values (colon not at end)
		trimmed := strings.TrimSpace(line)
		if strings.Contains(trimmed, ":") && !strings.HasSuffix(trimmed, ":") {
			result = append(result, line)
			continue
		}

		// Check if any descendant has a ref
		currentIndent := getIndentLevel(line)
		hasRefDescendant := false
		for j := i + 1; j < len(lines); j++ {
			childIndent := getIndentLevel(lines[j])
			if childIndent <= currentIndent {
				break
			}
			if strings.Contains(lines[j], "[ref=") {
				hasRefDescendant = true
				break
			}
		}
		if hasRefDescendant {
			result = append(result, line)
		}
	}

	return strings.Join(result, "\n")
}

// getIndentLevel returns the indentation level (number of 2-space indents).
func getIndentLevel(line string) int {
	spaces := 0
	for _, c := range line {
		if c == ' ' {
			spaces++
		} else {
			break
		}
	}
	return spaces / 2
}
