import { z } from "zod";
import { FAILURE_CODES, FailureError, type ToolDefinition } from "@argent/registry";
import { RN_ONLY_TOOL_CAPABILITY } from "../../debugger/debugger-service-ref";
import {
  REACT_PROFILER_SESSION_NAMESPACE,
  type ReactProfilerSessionApi,
} from "../../../blueprints/react-profiler-session";
import { HEARTBEAT_SCRIPT, FIBER_ROOT_TRACKER_SCRIPT } from "../../../utils/react-profiler/scripts";
import { NO_DEVTOOLS_HOOK_ERROR, NO_RENDERERS_ATTACHED_ERROR } from "./react-profiler-start";

const COLLECT_RENDERS_SCRIPT = `
(function() {
  try {
    var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    if (!hook) return JSON.stringify({ error: 'no __REACT_DEVTOOLS_GLOBAL_HOOK__' });

    var renderers = hook._renderers || hook.renderers;
    if (!renderers) return JSON.stringify({ error: 'no renderers attached to hook' });

    var results = {};

    function walkFiber(fiber, depth) {
      if (!fiber || depth > 30) return;
      var name = null;
      if (fiber.type) {
        if (typeof fiber.type === 'string') name = fiber.type;
        else if (fiber.type.displayName) name = fiber.type.displayName;
        else if (fiber.type.name) name = fiber.type.name;
      }
      if (name && fiber.actualDuration !== undefined) {
        if (!results[name]) results[name] = { renderCount: 0, totalActualDuration: 0, selfBaseDuration: 0 };
        results[name].renderCount += 1;
        results[name].totalActualDuration += fiber.actualDuration || 0;
        results[name].selfBaseDuration += fiber.selfBaseDuration || 0;
      }
      if (fiber.child) walkFiber(fiber.child, depth + 1);
      if (fiber.sibling) walkFiber(fiber.sibling, depth);
    }

    var roots = hook.__argent_roots__ || hook._fiberRoots || hook.fiberRoots;
    if (roots) {
      var iter = roots.values ? roots.values() : Object.values(roots);
      for (var root of iter) {
        if (root.current) walkFiber(root.current, 0);
      }
    }

    return JSON.stringify(results);
  } catch(e) {
    return JSON.stringify({ error: String(e) });
  }
})()
`;

const HOOK_MISSING_ERROR = "no __REACT_DEVTOOLS_GLOBAL_HOOK__";
const NO_RENDERERS_ERROR = "no renderers attached to hook";
const HOOK_NOT_PRESENT_ERRORS = new Set([HOOK_MISSING_ERROR, NO_RENDERERS_ERROR]);

// "Hook missing" and "renderers not attached" point at different runtime
// states and have different remediations. The two codes funnel into
// FIBER_ROOT_TRACKER_SCRIPT for the retry path, but the verbose throw
// branches on the actual code so the operator gets accurate guidance.
function messageForHookError(code: string): string {
  if (code === HOOK_MISSING_ERROR) return NO_DEVTOOLS_HOOK_ERROR;
  if (code === NO_RENDERERS_ERROR) return NO_RENDERERS_ATTACHED_ERROR;
  return `React hook error: ${code}`;
}

type ParsedRenders =
  | Record<
      string,
      {
        renderCount: number;
        totalActualDuration: number;
        selfBaseDuration: number;
      }
    >
  | { error: string };

interface RenderEntry {
  component: string;
  renderCount: number;
  totalActualDuration_ms: number;
  selfBaseDuration_ms: number;
}

function renderMarkdownTable(entries: RenderEntry[]): string {
  if (entries.length === 0)
    return "_No render data found. Ensure React DevTools global hook is present._";
  const header = "| Component | Renders | Total (ms) | Self Base (ms) |";
  const sep = "|---|---|---|---|";
  const rows = entries.map(
    (e) =>
      `| \`${e.component}\` | ${e.renderCount} | ${e.totalActualDuration_ms.toFixed(2)} | ${e.selfBaseDuration_ms.toFixed(2)} |`
  );
  return [header, sep, ...rows].join("\n");
}

const zodSchema = z.object({
  port: z.coerce.number().default(8081).describe("Metro server port"),
  device_id: z
    .string()
    .describe(
      "Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId)."
    ),
  top_n: z.coerce
    .number()
    .int()
    .positive()
    .default(20)
    .describe("Number of top re-rendering components to return (default 20)"),
});

export const reactProfilerRendersTool: ToolDefinition<z.infer<typeof zodSchema>, string> = {
  id: "react-profiler-renders",
  interaction: {
    startedMsg: () => "Reading React render activity",
    completedMsg: () => "Read React render activity",
    failedMsg: ({ failureSignal }) =>
      `Failed to read React render activity: ${failureSignal.error_code}`,
  },
  description: `Scan the live React fiber tree to collect component render counts and durations.
Returns a markdown table of the top re-rendering components. No profiling session required — works on a live connected app.
Use when you want a quick snapshot of render counts without a full profiling session.
Fails if the React DevTools hook is not present in the runtime or the app is not connected.`,
  zodSchema,
  // RN-only: queries the React DevTools backend hook on the live runtime.
  capability: RN_ONLY_TOOL_CAPABILITY,
  services: (params) => ({
    profilerSession: `${REACT_PROFILER_SESSION_NAMESPACE}:${params.port}:${params.device_id}`,
  }),
  async execute(services, params) {
    const api = services.profilerSession as ReactProfilerSessionApi;
    const cdp = api.cdp;

    if (api.profilingActive && api.ownerToolServerPid === process.pid) {
      await cdp.evaluate(HEARTBEAT_SCRIPT).catch(() => {});
    }

    type EvalResult = {
      result?: { value?: string };
      exceptionDetails?: { text?: string };
    };

    async function evalRenders(): Promise<EvalResult> {
      return cdp.send("Runtime.evaluate", {
        expression: COLLECT_RENDERS_SCRIPT,
        returnByValue: true,
        timeout: 5000,
      }) as Promise<EvalResult>;
    }

    let result = await evalRenders();

    if (result?.exceptionDetails) {
      throw new FailureError(`Runtime exception: ${result.exceptionDetails.text ?? "unknown"}`, {
        error_code: FAILURE_CODES.REACT_PROFILER_RUNTIME_EXCEPTION,
        failure_stage: "react_profiler_renders_runtime_eval",
        failure_area: "tool_server",
        error_kind: "subprocess",
      });
    }

    if (!result?.result?.value) {
      throw new FailureError("No data returned from runtime evaluation.", {
        error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
        failure_stage: "react_profiler_renders_runtime_eval",
        failure_area: "tool_server",
        error_kind: "subprocess",
      });
    }

    let parsed = JSON.parse(result.result.value) as ParsedRenders;

    function getErrorString(p: ParsedRenders): string | null {
      if ("error" in p && typeof (p as { error?: unknown }).error === "string") {
        return (p as { error: string }).error;
      }
      return null;
    }

    // Re-inject hook once if missing and retry
    const firstError = getErrorString(parsed);
    if (firstError !== null && HOOK_NOT_PRESENT_ERRORS.has(firstError)) {
      await cdp.evaluate(FIBER_ROOT_TRACKER_SCRIPT).catch(() => {});
      result = await evalRenders();
      if (result?.result?.value) {
        parsed = JSON.parse(result.result.value) as ParsedRenders;
      }
    }

    const errorStr = getErrorString(parsed);
    if (errorStr !== null) {
      throw new FailureError(messageForHookError(errorStr), {
        error_code: HOOK_NOT_PRESENT_ERRORS.has(errorStr)
          ? FAILURE_CODES.REACT_PROFILER_DEVTOOLS_HOOK_MISSING
          : FAILURE_CODES.REACT_PROFILER_HOOK_ERROR,
        failure_stage: "react_profiler_renders_hook_read",
        failure_area: "tool_server",
        error_kind: "validation",
      });
    }

    const entries: RenderEntry[] = Object.entries(parsed)
      .map(([component, data]) => ({
        component,
        renderCount: data.renderCount,
        totalActualDuration_ms: data.totalActualDuration,
        selfBaseDuration_ms: data.selfBaseDuration,
      }))
      .sort((a, b) => b.totalActualDuration_ms - a.totalActualDuration_ms)
      .slice(0, params.top_n);

    return `## React Component Renders\n\n${renderMarkdownTable(entries)}`;
  },
};
