import * as crypto from "node:crypto";
import { z } from "zod";
import {
  FAILURE_CODES,
  FailureError,
  type Registry,
  type ToolDefinition,
  ServiceState,
} from "@argent/registry";
import {
  REACT_PROFILER_SESSION_NAMESPACE,
  type ReactProfilerSessionApi,
  clearCachedProfilerPaths,
} from "../../../blueprints/react-profiler-session";
import { JS_RUNTIME_DEBUGGER_NAMESPACE } from "../../../blueprints/js-runtime-debugger";
import {
  REACT_NATIVE_PROFILER_SETUP_SCRIPT,
  READ_STATE_SCRIPT,
  BOOTSTRAP_DEVTOOLS_BACKEND_SCRIPT,
  buildStartScript,
  STOP_FOR_TAKEOVER_SCRIPT,
} from "../../../utils/react-profiler/scripts";
import {
  classifyStaleness,
  DEFAULT_STALE_THRESHOLD_MS,
  type ProfilerSessionOwner,
} from "../../../utils/react-profiler/session-ownership";
import { RN_ONLY_TOOL_CAPABILITY } from "../../debugger/debugger-service-ref";
import { canonicalDeviceId } from "../../../utils/debugger/device-alias";
import {
  bootstrapFailureMessage,
  type BootstrapResult,
} from "../../../utils/react-profiler/devtools-bootstrap";

/**
 * Verbose explanations the operator sees when the runtime is not profileable.
 * Centralised so every tool that detects "this app cannot be profiled" emits
 * the same diagnosis instead of bespoke one-liners.
 */
export const NO_DEVTOOLS_HOOK_ERROR =
  "React DevTools hook (__REACT_DEVTOOLS_GLOBAL_HOOK__) is not present in this app's JavaScript runtime. " +
  "React profiling requires a development build with React DevTools enabled. " +
  "Likely causes: (1) the app is a release/production build — DevTools is stripped to reduce bundle size; " +
  "(2) you connected to the wrong JS runtime; (3) this isn't a React (Native) app. " +
  "Fix: rebuild in debug/dev mode (e.g. `npx react-native run-ios` without --configuration Release; for Expo, run a dev client). " +
  "Once the app is running with DevTools attached, call react-profiler-start again.";

/**
 * Returned when the DevTools hook IS present but no React renderer has
 * registered against it. Distinct from NO_DEVTOOLS_HOOK_ERROR because the
 * remediation differs: rebuilding in dev mode does nothing here — the user
 * needs the renderer to attach (wait for first commit, or let
 * react-profiler-start bootstrap the DevTools backend on bridgeless RN
 * dev builds that lack an external DevTools client).
 */
export const NO_RENDERERS_ATTACHED_ERROR =
  "React DevTools hook is present but no React renderer has registered yet. " +
  "The hook is loaded but no fiber renderer has attached — typically because the app has not committed its first render, or the DevTools backend has not been bootstrapped on a bridgeless React Native dev build. " +
  "Fix: ensure the app has rendered (interact with it once, then retry); if it stays empty, call react-profiler-start first — it will attempt to attach the DevTools backend automatically.";

const zodSchema = z.object({
  port: z.coerce.number().default(8081).describe("Metro server port"),
  device_id: z
    .string()
    .describe(
      "Device id from list-devices — the SAME id you passed to debugger-connect (iOS simulator UDID or Android serial)."
    ),
  sample_interval_us: z.coerce
    .number()
    .int()
    .positive()
    .default(100)
    .describe("CPU sampling interval in microseconds (default 100)"),
  force: z
    .boolean()
    .default(false)
    .describe(
      "Take over an active profiling session even when it is owned by another tool-server and still fresh. Set to true only when you know the prior owner is gone."
    ),
});

type ReadStateResult =
  | { hookExists: false }
  | { hookExists: true; rendererInterfaceFound: false }
  | {
      hookExists: true;
      rendererInterfaceFound: true;
      isRunning: boolean;
      owner: ProfilerSessionOwner | null;
      nowEpochMs: number;
    };

function safeGetState(registry: Registry, urn: string): ServiceState | null {
  try {
    return registry.getServiceState(urn);
  } catch {
    return null;
  }
}

export function createReactProfilerStartTool(
  registry: Registry
): ToolDefinition<z.infer<typeof zodSchema>, Record<string, unknown>> {
  return {
    id: "react-profiler-start",
    interaction: {
      startedMsg: () => "Starting React profiler",
      completedMsg: ({ result }) =>
        result.already_running ? "React profiler was already running" : "Started React profiler",
      failedMsg: ({ failureSignal }) =>
        `Failed to start React profiler: ${failureSignal.error_code}`,
    },
    description: `Start CPU profiling + React commit capture on the connected Hermes runtime.
Delegates React commit capture to the in-app React DevTools backend (ri.startProfiling).
If another tool-server already owns the session, returns { already_running: true, owner, stale, how_to_reclaim } without clobbering their data. Pass { force: true } to reclaim a fresh owner's session, but BEFORE OVERTAKING - ask the user for approval first, see relevant skill for guidance.
Before calling this, ask the user if they also want native profiling (native-profiler-start) — recommend running both in parallel for a complete picture.
After starting, ask the user to perform the interaction to profile, then call react-profiler-stop.
Returns { started_at, startedAtEpochMs, hermes_version, detected_architecture } on success, or the already_running payload described above.
Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot be established.`,
    zodSchema,
    // RN-only: bootstraps the React DevTools backend and uses Hermes'
    // Profiler.start. A CDP-direct CPU profile for Chromium is tracked as a
    // follow-up; the React commit recording has no Chromium analog.
    capability: RN_ONLY_TOOL_CAPABILITY,
    services: () => ({}),
    async execute(_services, params) {
      // Canonicalize a forwarded logicalDeviceId to the connect id so the
      // profiler session and its debugger dependency reuse the open connection.
      const deviceId = canonicalDeviceId(params.device_id);
      const jsdUrn = `${JS_RUNTIME_DEBUGGER_NAMESPACE}:${params.port}:${deviceId}`;
      const psUrn = `${REACT_PROFILER_SESSION_NAMESPACE}:${params.port}:${deviceId}`;
      const ignore = () => {};

      async function disposeAndWait() {
        try {
          await registry.disposeService(psUrn);
        } catch {
          /* ignore */
        }
        try {
          await registry.disposeService(jsdUrn);
        } catch {
          /* ignore */
        }
        const deadline = Date.now() + 3000;
        while (Date.now() < deadline) {
          const jsdState = safeGetState(registry, jsdUrn);
          const psState = safeGetState(registry, psUrn);
          if (jsdState !== ServiceState.TERMINATING && psState !== ServiceState.TERMINATING) break;
          await new Promise((r) => setTimeout(r, 50));
        }
      }

      const snapshot = registry.getSnapshot();
      const psEntry = snapshot.services.get(psUrn);
      const jsdEntry = snapshot.services.get(jsdUrn);
      if (
        (psEntry &&
          psEntry.state !== ServiceState.RUNNING &&
          psEntry.state !== ServiceState.IDLE) ||
        (jsdEntry &&
          jsdEntry.state !== ServiceState.RUNNING &&
          jsdEntry.state !== ServiceState.IDLE)
      ) {
        await disposeAndWait();
      }

      let api = await registry.resolveService<ReactProfilerSessionApi>(psUrn);
      if (!api.cdp.isConnected()) {
        await disposeAndWait();
        api = await registry.resolveService<ReactProfilerSessionApi>(psUrn);
        if (!api.cdp.isConnected()) {
          throw new FailureError(
            "CDP connection not available. The Hermes runtime may still be loading. Call react-profiler-start again.",
            {
              error_code: FAILURE_CODES.REACT_PROFILER_CDP_NOT_CONNECTED,
              failure_stage: "react_profiler_start_cdp_connection",
              failure_area: "tool_server",
              error_kind: "network",
            }
          );
        }
      }

      const cdp = api.cdp;

      // Inject the native-profiler instrumentation (idempotent).
      await cdp.evaluate(REACT_NATIVE_PROFILER_SETUP_SCRIPT);

      // Snapshot backend state so we can decide whether to start, take over, or refuse.
      let stateJson = (await cdp.evaluate(READ_STATE_SCRIPT)) as string | undefined;
      if (!stateJson) {
        throw new FailureError(
          "Failed to read React profiler state from runtime (no value returned).",
          {
            error_code: FAILURE_CODES.REACT_PROFILER_STATE_READ_FAILED,
            failure_stage: "react_profiler_start_read_state",
            failure_area: "tool_server",
            error_kind: "subprocess",
          }
        );
      }
      let state = JSON.parse(stateJson) as ReadStateResult;

      if (!state.hookExists) {
        throw new FailureError(NO_DEVTOOLS_HOOK_ERROR, {
          error_code: FAILURE_CODES.REACT_PROFILER_DEVTOOLS_HOOK_MISSING,
          failure_stage: "react_profiler_start_devtools_hook",
          failure_area: "tool_server",
          error_kind: "validation",
        });
      }

      // If the hook is present but no rendererInterface is registered, the
      // React DevTools backend hasn't called `attach()` yet — typically because
      // no external DevTools client (Fusebox React tab, `npx react-devtools`)
      // is connected in a bridgeless RN dev build. Try to bootstrap it
      // ourselves via react-devtools-core; fall back to an actionable error
      // identifying the specific failure mode (production build, rdt-core
      // version too old, etc.).
      if (!("rendererInterfaceFound" in state) || !state.rendererInterfaceFound) {
        const bootstrapJson = (await cdp.evaluate(BOOTSTRAP_DEVTOOLS_BACKEND_SCRIPT)) as
          | string
          | undefined;
        if (!bootstrapJson) {
          throw new FailureError(
            "Failed to attach React DevTools backend (no value returned from runtime).",
            {
              error_code: FAILURE_CODES.REACT_PROFILER_DEVTOOLS_BACKEND_ATTACH_FAILED,
              failure_stage: "react_profiler_start_bootstrap_backend",
              failure_area: "tool_server",
              error_kind: "subprocess",
            }
          );
        }
        const bootstrap = JSON.parse(bootstrapJson) as BootstrapResult;

        if (!bootstrap.ok) {
          throw new FailureError(bootstrapFailureMessage(bootstrap), {
            error_code: FAILURE_CODES.REACT_PROFILER_DEVTOOLS_BACKEND_BOOTSTRAP_FAILED,
            failure_stage: "react_profiler_start_bootstrap_backend",
            failure_area: "tool_server",
            error_kind: "subprocess",
          });
        }

        // Re-run the setup script: it walks `hook.rendererInterfaces` and
        // installs the `__argent_startWrapped__` wrappers. The previous setup
        // call (before bootstrap) saw an empty map and did nothing, so the
        // freshly-attached interfaces are unwrapped — `buildStartScript`'s
        // post-start check on `__argent_isProfiling__` would fail without this.
        await cdp.evaluate(REACT_NATIVE_PROFILER_SETUP_SCRIPT);

        stateJson = (await cdp.evaluate(READ_STATE_SCRIPT)) as string | undefined;
        if (!stateJson) {
          throw new FailureError(
            "Failed to re-read React profiler state after attach (no value returned).",
            {
              error_code: FAILURE_CODES.REACT_PROFILER_STATE_READ_FAILED,
              failure_stage: "react_profiler_start_reread_state",
              failure_area: "tool_server",
              error_kind: "subprocess",
            }
          );
        }
        state = JSON.parse(stateJson) as ReadStateResult;

        if (
          !state.hookExists ||
          !("rendererInterfaceFound" in state) ||
          !state.rendererInterfaceFound
        ) {
          throw new FailureError(
            "Attached the React DevTools backend but no React renderer registered itself afterwards. Ask the user to fully reload the JS bundle and retry.",
            {
              error_code: FAILURE_CODES.REACT_PROFILER_DEVTOOLS_RENDERER_MISSING,
              failure_stage: "react_profiler_start_renderer_check",
              failure_area: "tool_server",
              error_kind: "validation",
            }
          );
        }
      }

      // If a session is already active, classify it and decide.
      if (state.isRunning) {
        const owner: ProfilerSessionOwner | null = state.owner;
        const staleness = classifyStaleness({
          owner,
          nowEpochMs: state.nowEpochMs,
          staleThresholdMs: DEFAULT_STALE_THRESHOLD_MS,
        });

        const canTakeOverSilently = staleness.canReclaimWithoutForce || params.force === true;
        if (!canTakeOverSilently) {
          return {
            already_running: true,
            owner,
            age_seconds: staleness.ageSeconds,
            stale: staleness.stale,
            how_to_reclaim:
              "Another profiling session is already active — see the `owner` field. Ask the user before taking over and explain that the prior session's data will be discarded; to take over, call react-profiler-start again with { force: true }. If `stale` is true, the prior owner is likely gone and you may take over without prompting the user.",
          };
        }

        // Reclaim path: stop the prior session so we can start cleanly.
        await cdp.evaluate(STOP_FOR_TAKEOVER_SCRIPT).catch(ignore);
      }

      // Defensive: if Hermes thinks it's already sampling CPU, stop before re-starting.
      if (api.profilingActive) {
        await cdp.send("Profiler.stop").catch(ignore);
        api.profilingActive = false;
      }

      const sessionId = crypto.randomUUID();
      const ownerPayload: ProfilerSessionOwner = {
        sessionId,
        // startedAtEpochMs/lastHeartbeatEpochMs set inside the script using
        // the wrapper-captured value to eliminate clock skew.
        startedAtEpochMs: 0,
        lastHeartbeatEpochMs: 0,
      };

      await cdp.send("Profiler.enable").catch(ignore);
      await cdp.send("Profiler.start", { interval: params.sample_interval_us });

      const startJson = (await cdp.evaluate(buildStartScript(JSON.stringify(ownerPayload)))) as
        | string
        | undefined;
      if (!startJson) {
        throw new FailureError("Failed to start React profiler (no value returned from runtime).", {
          error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
          failure_stage: "react_profiler_start_runtime_start",
          failure_area: "tool_server",
          error_kind: "subprocess",
        });
      }
      const startResult = JSON.parse(startJson) as {
        ok: boolean;
        reason?: string;
        message?: string;
        startedAtEpochMs?: number;
        isProfilingFlagSet?: boolean;
        ownerInstalled?: boolean;
      };

      if (!startResult.ok) {
        // Roll back CPU sampler so we don't leak state.
        await cdp.send("Profiler.stop").catch(ignore);
        throw new FailureError(
          `React profiler failed to start (${startResult.reason ?? "unknown"}${
            startResult.message ? `: ${startResult.message}` : ""
          })`,
          {
            error_code: FAILURE_CODES.REACT_PROFILER_START_FAILED,
            failure_stage: "react_profiler_start_runtime_start",
            failure_area: "tool_server",
            error_kind: "subprocess",
          }
        );
      }

      if (startResult.isProfilingFlagSet !== true || startResult.ownerInstalled !== true) {
        await cdp.send("Profiler.stop").catch(ignore);
        throw new FailureError(
          `React profiler failed to start (post-start verification failed: isProfilingFlagSet=${startResult.isProfilingFlagSet === true}, ownerInstalled=${startResult.ownerInstalled === true})`,
          {
            error_code: FAILURE_CODES.REACT_PROFILER_START_VERIFY_FAILED,
            failure_stage: "react_profiler_start_verify",
            failure_area: "tool_server",
            error_kind: "subprocess",
          }
        );
      }

      const startedAtEpochMs = startResult.startedAtEpochMs ?? Date.now();

      clearCachedProfilerPaths(api.port, api.deviceId);
      api.sessionPaths = null;
      api.profilingActive = true;
      api.anyCompilerOptimized = null;
      api.hotCommitIndices = null;
      api.totalReactCommits = null;
      api.profileStartWallMs = startedAtEpochMs;
      api.sessionId = sessionId;
      api.ownerToolServerPid = process.pid;

      return {
        started_at: new Date(startedAtEpochMs).toISOString(),
        startedAtEpochMs,
        hermes_version: api.hermesVersion,
        detected_architecture: api.detectedArchitecture,
      };
    },
  };
}
