import { z } from "zod";

export const describeFrameSchema = z.object({
  x: z.number().finite().min(0).max(1),
  y: z.number().finite().min(0).max(1),
  width: z.number().finite().min(0).max(1),
  height: z.number().finite().min(0).max(1),
});

export type DescribeFrame = z.infer<typeof describeFrameSchema>;

export interface DescribeNode {
  role: string;
  frame: DescribeFrame;
  children: DescribeNode[];
  label?: string;
  identifier?: string;
  value?: string;
  // Text hoisted up from descendant nodes during the flow adapters' flatten
  // pass (see flow-ios-tree / flow-android-tree). The flat-leaves shape
  // discards nesting, so a testID container's own `label`/`value` is empty even
  // when it visibly wraps text (e.g. a counter whose number is a child `Text`).
  // `subtreeText` carries that descendant text so an `assert`/`text` check can
  // read "what this container shows" without the structure. Only the flow trees
  // populate it; the agent-facing describe path leaves it unset.
  subtreeText?: string;
  // Interactivity flags surfaced by the Android uiautomator dump. iOS
  // consumers leave these unset; adding them as optional avoids breaking
  // existing payloads. `scrollHidden` counts children that fell outside an
  // ancestor scroll's clip rect — the agent should swipe before tapping.
  clickable?: boolean;
  longClickable?: boolean;
  scrollable?: boolean;
  checkable?: boolean;
  checked?: boolean;
  disabled?: boolean;
  password?: boolean;
  scrollHidden?: number;
  // Vega (Fire TV) is D-pad driven, so "where is the cursor" is the key signal.
  // `focused` is the element holding input focus; `selected` is the visually
  // highlighted / active item (e.g. the current nav tab). On Vega the toolkit
  // often reports the highlighted item via `selected` while `focused` stays
  // false, so both are surfaced. Other platforms leave these unset.
  focused?: boolean;
  selected?: boolean;
}

export const describeNodeSchema: z.ZodType<DescribeNode> = z.lazy(() =>
  z
    .object({
      role: z.string().min(1),
      frame: describeFrameSchema,
      children: z.array(describeNodeSchema),
      label: z.string().optional(),
      identifier: z.string().optional(),
      value: z.string().optional(),
      subtreeText: z.string().optional(),
      clickable: z.boolean().optional(),
      longClickable: z.boolean().optional(),
      scrollable: z.boolean().optional(),
      checkable: z.boolean().optional(),
      checked: z.boolean().optional(),
      disabled: z.boolean().optional(),
      password: z.boolean().optional(),
      scrollHidden: z.number().int().nonnegative().optional(),
      focused: z.boolean().optional(),
      selected: z.boolean().optional(),
    })
    .passthrough()
);

// Where the tree came from. "ax-service" / "native-devtools" come from iOS;
// "uiautomator" / "android-devtools" come from Android; "cdp-dom" is the
// Chromium branch's DOM walk over Chrome DevTools Protocol; "vega-automation"
// is the Vega on-device automation toolkit; "tv-focus" is the focus-driven view
// returned for a TV target (Apple TV / Android TV), which reports focused /
// focusable elements rather than a tap-oriented tree. Agents that branch on
// `source` (e.g. to decide whether to also call `native-find-views` for a
// richer tree) need to distinguish each provider — which a shared label would
// hide.
export type DescribeSource =
  | "ax-service"
  | "native-devtools"
  | "uiautomator"
  | "android-devtools"
  | "cdp-dom"
  | "vega-automation"
  | "tv-focus";

// Internal shape produced by the per-platform adapters. The `tree` is consumed
// by the formatter in `format-tree.ts` and then dropped before the tool replies
// — callers see `DescribeResult` below, which surfaces only the rendered text.
export interface DescribeTreeData {
  tree: DescribeNode;
  source: DescribeSource;
  should_restart?: boolean;
  hint?: string;
  // Screen size the frames were normalized against, in the source's native
  // units (Android px, iOS pt) — only the aspect ratio is comparable across
  // sources. Optional: populated by the flow tree adapters that know it (the
  // rotate directive's physical-circle geometry reads it); other paths leave
  // it unset.
  screen?: { width: number; height: number };
}

// Public describe-tool response. The full JSON `tree` (the previous payload's
// biggest cost — ~6× the byte size of the formatted rendering on a typical iOS
// screen) is no longer surfaced; `description` is a text rendering produced by
// `format-tree.ts` that preserves every label, role, and frame the agent needs
// for taps.
export interface DescribeResult {
  description: string;
  source: DescribeSource;
  should_restart?: boolean;
  hint?: string;
}

export function parseDescribeResult(input: unknown): DescribeNode {
  return describeNodeSchema.parse(input);
}

export function getDescribeTapPoint(frame: DescribeFrame): { x: number; y: number } {
  return {
    x: frame.x + frame.width / 2,
    y: frame.y + frame.height / 2,
  };
}
