package main

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

type AXProbeResult struct {
	AppName string   `json:"app_name"`
	PID     int      `json:"pid"`
	Nodes   []AXNode `json:"nodes"`
}

type AXNode struct {
	Role        string     `json:"role"`
	Title       string     `json:"title"`
	Value       string     `json:"value"`
	Description string     `json:"description"`
	Frame       [4]float64 `json:"frame"`
	Depth       int        `json:"depth"`
}

func runAXProbe() error {
	logInfo("macOS Accessibility 微信 UI 树探测启动")

	config, err := loadConfig("config.toml")
	if err != nil {
		return fmt.Errorf("读取 config.toml 失败：%w", err)
	}

	regionsPath := appDataPath(config, config.Agent.RegionsPath)
	annotation, err := loadAnnotation(regionsPath)
	if err != nil {
		return fmt.Errorf("读取标注文件失败：%w", err)
	}
	summaries, warnings := inferMissingRegionTypes(summarizeRegions(annotation.Regions))
	for _, warning := range warnings {
		logWarning(warning)
	}

	probe, err := probeWeChatAXTree()
	if err != nil {
		return err
	}
	logInfo(fmt.Sprintf("AX 节点总数=%d pid=%d app=%s", len(probe.Nodes), probe.PID, probe.AppName))

	textNodes := usefulAXTextNodes(probe.Nodes)
	logInfo(fmt.Sprintf("可读文本/按钮节点=%d", len(textNodes)))
	for index, node := range firstNodes(textNodes, 80) {
		logJSON("ax", fmt.Sprintf("#%02d depth=%d role=%s frame=[%.0f,%.0f,%.0f,%.0f] text=%s", index+1, node.Depth, node.Role, node.Frame[0], node.Frame[1], node.Frame[2], node.Frame[3], axNodeText(node)))
	}

	if chat, ok := regionByType(summaries)["chat_content"]; ok {
		chatNodes := nodesInsideBox(textNodes, chat.BBoxScreen)
		sort.SliceStable(chatNodes, func(i, j int) bool {
			if chatNodes[i].Frame[1] == chatNodes[j].Frame[1] {
				return chatNodes[i].Frame[0] < chatNodes[j].Frame[0]
			}
			return chatNodes[i].Frame[1] < chatNodes[j].Frame[1]
		})
		logInfo(fmt.Sprintf("chat_content 区域内可读节点=%d", len(chatNodes)))
		for index, node := range firstNodes(chatNodes, 80) {
			logJSON("ax-chat", fmt.Sprintf("#%02d role=%s frame=[%.0f,%.0f,%.0f,%.0f] text=%s", index+1, node.Role, node.Frame[0], node.Frame[1], node.Frame[2], node.Frame[3], axNodeText(node)))
		}
	} else {
		logWarning("未找到 chat_content 标注区域，跳过区域过滤")
	}

	return nil
}

func probeWeChatAXTree() (AXProbeResult, error) {
	script := `
import AppKit
import ApplicationServices
import Foundation

struct Node: Encodable {
  let role: String
  let title: String
  let value: String
  let description: String
  let frame: [Double]
  let depth: Int
}

struct Result: Encodable {
  let app_name: String
  let pid: Int
  let nodes: [Node]
}

func stringAttr(_ element: AXUIElement, _ key: String) -> String {
  var value: CFTypeRef?
  let error = AXUIElementCopyAttributeValue(element, key as CFString, &value)
  if error != .success || value == nil { return "" }
  if let str = value as? String { return clean(str) }
  if let attr = value as? NSAttributedString { return clean(attr.string) }
  return ""
}

func clean(_ value: String) -> String {
  return value
    .replacingOccurrences(of: "\n", with: " ")
    .replacingOccurrences(of: "\r", with: " ")
    .replacingOccurrences(of: "\t", with: " ")
}

func frameOf(_ element: AXUIElement) -> [Double] {
  var positionValue: CFTypeRef?
  var sizeValue: CFTypeRef?
  var point = CGPoint.zero
  var size = CGSize.zero
  if AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &positionValue) == .success,
     let positionValue,
     CFGetTypeID(positionValue) == AXValueGetTypeID() {
    AXValueGetValue(positionValue as! AXValue, .cgPoint, &point)
  }
  if AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizeValue) == .success,
     let sizeValue,
     CFGetTypeID(sizeValue) == AXValueGetTypeID() {
    AXValueGetValue(sizeValue as! AXValue, .cgSize, &size)
  }
  return [Double(point.x), Double(point.y), Double(size.width), Double(size.height)]
}

func childrenOf(_ element: AXUIElement) -> [AXUIElement] {
  var value: CFTypeRef?
  let error = AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value)
  if error != .success || value == nil { return [] }
  return value as? [AXUIElement] ?? []
}

func walk(_ element: AXUIElement, depth: Int, nodes: inout [Node], visited: inout Int) {
  if visited > 2500 || depth > 20 { return }
  visited += 1
  let node = Node(
    role: stringAttr(element, kAXRoleAttribute),
    title: stringAttr(element, kAXTitleAttribute),
    value: stringAttr(element, kAXValueAttribute),
    description: stringAttr(element, kAXDescriptionAttribute),
    frame: frameOf(element),
    depth: depth
  )
  nodes.append(node)
  for child in childrenOf(element) {
    walk(child, depth: depth + 1, nodes: &nodes, visited: &visited)
  }
}

let trusted = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)
if !trusted {
  fputs("Accessibility permission is not granted\n", stderr)
  exit(10)
}

let apps = NSWorkspace.shared.runningApplications.filter { app in
  let name = app.localizedName ?? ""
  return name == "微信" || name == "WeChat" || name.lowercased().contains("wechat")
}

guard let app = apps.first else {
  fputs("WeChat app not found\n", stderr)
  exit(2)
}

app.activate(options: [.activateIgnoringOtherApps])
usleep(300000)

let root = AXUIElementCreateApplication(app.processIdentifier)
var nodes: [Node] = []
var visited = 0
walk(root, depth: 0, nodes: &nodes, visited: &visited)
let result = Result(app_name: app.localizedName ?? "", pid: Int(app.processIdentifier), nodes: nodes)
let data = try JSONEncoder().encode(result)
print(String(data: data, encoding: .utf8)!)
`

	output, err := runSwift(script)
	if err != nil {
		return AXProbeResult{}, fmt.Errorf("AX probe failed: %w", err)
	}

	var result AXProbeResult
	if err := json.Unmarshal([]byte(output), &result); err != nil {
		preview := output
		if len(preview) > 300 {
			preview = preview[:300]
		}
		return AXProbeResult{}, fmt.Errorf("parse AX JSON failed: %w output_prefix=%q", err, preview)
	}
	return result, nil
}

func usefulAXTextNodes(nodes []AXNode) []AXNode {
	result := make([]AXNode, 0)
	seen := map[string]bool{}
	for _, node := range nodes {
		text := strings.TrimSpace(axNodeText(node))
		if text == "" {
			continue
		}
		if node.Frame[2] <= 0 || node.Frame[3] <= 0 {
			continue
		}
		key := fmt.Sprintf("%s|%s|%.0f|%.0f|%.0f|%.0f", node.Role, text, node.Frame[0], node.Frame[1], node.Frame[2], node.Frame[3])
		if seen[key] {
			continue
		}
		seen[key] = true
		result = append(result, node)
	}
	sort.SliceStable(result, func(i, j int) bool {
		if result[i].Frame[1] == result[j].Frame[1] {
			return result[i].Frame[0] < result[j].Frame[0]
		}
		return result[i].Frame[1] < result[j].Frame[1]
	})
	return result
}

func axNodeText(node AXNode) string {
	parts := []string{node.Value, node.Title, node.Description}
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part != "" {
			return part
		}
	}
	return ""
}

func nodesInsideBox(nodes []AXNode, box [4]float64) []AXNode {
	result := make([]AXNode, 0)
	for _, node := range nodes {
		cx := node.Frame[0] + node.Frame[2]/2
		cy := node.Frame[1] + node.Frame[3]/2
		if cx >= box[0] && cx <= box[2] && cy >= box[1] && cy <= box[3] {
			result = append(result, node)
		}
	}
	return result
}

func firstNodes(nodes []AXNode, limit int) []AXNode {
	if len(nodes) <= limit {
		return nodes
	}
	return nodes[:limit]
}
