import { useEffect, useMemo, useRef, useState } from "react";
import { useAtomValue, useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { toast } from "sonner";
import {
  ToolAddress,
  type Connection,
  type ConnectionName,
  type IntegrationSlug,
} from "@executor-js/sdk/shared";

import { executeCode, toolSchemaAtom } from "../api/atoms";
import { trackEvent } from "../api/analytics";
import { Badge } from "./badge";
import { Button } from "./button";
import { Label } from "./label";
import { Textarea } from "./textarea";
import { CodeBlock } from "./code-block";
import { CardStack, CardStackHeader, CardStackContent } from "./card-stack";
import { NativeSelect, NativeSelectOption } from "./native-select";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
import {
  JsonSchemaForm,
  isRenderableObjectSchema,
  missingRequiredFields,
} from "./json-schema-form";
import { cn } from "../lib/utils";

// ---------------------------------------------------------------------------
// ToolRunPanel — invoke a single tool against a chosen connection (account) to
// verify the credentials actually work and see the result/error.
//
// Backend: `POST /executions` runs codemode source. We generate a one-liner
// that invokes the exact per-connection tool through the sandbox `tools` proxy:
//
//   return await tools["<int>.<owner>.<conn>.<tool>"](<args>);
//
// The `<owner>` segment is the SELECTED CONNECTION's own owner — never an
// ambient value — because the connection list merges both owners (the global
// owner toggle is retired). The tool name segment may itself contain dots (e.g.
// `aliases.deleteAlias`), so the address is built by string-joining the segments
// and passed to the proxy via bracket access — never split on dots.
// ---------------------------------------------------------------------------

/** Build the proxy address (no `tools.` prefix) for a tool against a specific
 *  connection, using the CONNECTION's own owner. Pure — unit-testable. */
export const toolProxyAddress = (input: {
  readonly integration: IntegrationSlug;
  readonly connection: Connection;
  readonly toolName: string;
}): string =>
  `${input.integration}.${input.connection.owner}.${input.connection.name}.${input.toolName}`;

export const shouldRenderConnectionPicker = (connections: readonly Connection[]): boolean =>
  connections.length > 1;

type RunResult =
  | {
      readonly kind: "completed";
      readonly text: string;
      readonly structured: unknown;
      readonly isError: boolean;
    }
  | { readonly kind: "paused"; readonly text: string };

/** Detect the `{ ok: false }` invocation-failure envelope — a failed-credential
 *  signal even when the HTTP execution itself "completed" without isError. */
const isFailedEnvelope = (structured: unknown): boolean =>
  typeof structured === "object" &&
  structured !== null &&
  "ok" in structured &&
  (structured as { ok: unknown }).ok === false;

/** Decode the args Textarea as JSON. Effect Schema is used at the JSON boundary
 *  (the codebase forbids raw `JSON.parse`); a malformed string yields `None`,
 *  which we surface as an inline validation error. */
const decodeArgsJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);

const prettyJson = (value: unknown): string => {
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
};

export function ToolRunPanel(props: {
  /** Integration slug — the `<integration>` address segment. */
  readonly integration: IntegrationSlug;
  /** Bare tool name — the `<tool>` address segment (may contain dots). */
  readonly toolName: string;
  /** This integration's connections (accounts) across BOTH owners. Each is
   *  addressed under its OWN owner — there is no ambient owner. */
  readonly connections: readonly Connection[];
  /** Pre-select this connection (the account the selected tool belongs to in the
   *  grouped Tools tab). Falls back to the first connection when null/unmatched. */
  readonly initialConnectionName?: ConnectionName | string | null;
}) {
  const { integration, toolName, connections, initialConnectionName } = props;

  const [selectedConnection, setSelectedConnection] = useState<ConnectionName | null>(
    (initialConnectionName as ConnectionName | null) ?? connections[0]?.name ?? null,
  );
  const [argsJson, setArgsJson] = useState("{}");
  const [argsError, setArgsError] = useState<string | null>(null);
  const [argsMode, setArgsMode] = useState<"form" | "json">("json");
  const [running, setRunning] = useState(false);
  const [result, setResult] = useState<RunResult | null>(null);

  const doExecute = useAtomSet(executeCode, { mode: "promiseExit" });

  // Reset the picker + result when the connection set or the tool changes, so a
  // result never lingers against a stale tool/account. Prefer the tool's own
  // account (`initialConnectionName`) when it is present in the list.
  useEffect(() => {
    setSelectedConnection((current) => {
      if (
        initialConnectionName &&
        connections.some((c: Connection) => c.name === initialConnectionName)
      ) {
        return initialConnectionName as ConnectionName;
      }
      if (current && connections.some((c: Connection) => c.name === current)) return current;
      return connections[0]?.name ?? null;
    });
  }, [connections, initialConnectionName]);

  useEffect(() => {
    setResult(null);
    setArgsError(null);
  }, [toolName]);

  // The full `Connection` for the selected name — carries its OWN owner, which
  // is the `<owner>` address segment (never an ambient owner).
  const selectedConnectionObj = useMemo<Connection | null>(
    () => connections.find((c: Connection) => c.name === selectedConnection) ?? null,
    [connections, selectedConnection],
  );

  // Build the per-connection address `tools.<int>.<owner>.<conn>.<tool>` and the
  // proxy form (the same string minus the `tools.` root), using the SELECTED
  // CONNECTION's own owner.
  const addressNoPrefix = useMemo(
    () =>
      selectedConnectionObj
        ? toolProxyAddress({ integration, connection: selectedConnectionObj, toolName })
        : null,
    [integration, selectedConnectionObj, toolName],
  );

  const fullAddress = addressNoPrefix ? ToolAddress.make(`tools.${addressNoPrefix}`) : null;

  // The tool's schema view drives both the args-shape hint (TypeScript) and the
  // dynamic form (raw input JSON Schema + shared $defs).
  const schemaResult = useAtomValue(toolSchemaAtom(fullAddress ?? ToolAddress.make("")));
  const schemaView = AsyncResult.isSuccess(schemaResult) ? schemaResult.value : null;
  const inputSchema = schemaView?.inputSchema ?? null;
  const schemaDefinitions = schemaView?.schemaDefinitions;
  const schemaAvailable = isRenderableObjectSchema(inputSchema, schemaDefinitions);

  // The form is a controlled view over the single `argsJson` string. Decode it
  // (same Option-returning decode the run path uses); only a plain JSON object
  // can drive the form.
  const parsedArgs = useMemo<Record<string, unknown> | null>(() => {
    const decoded = decodeArgsJson(argsJson);
    if (Option.isNone(decoded)) return null;
    const v = decoded.value;
    return typeof v === "object" && v !== null && !Array.isArray(v)
      ? (v as Record<string, unknown>)
      : null;
  }, [argsJson]);

  // Required-arg gate: Run is only available once every `required` top-level
  // field has a value. With no renderable schema we can't know what's required,
  // so we don't block.
  const requiredMissing = useMemo<readonly string[]>(
    () =>
      schemaAvailable
        ? missingRequiredFields(inputSchema, schemaDefinitions, parsedArgs ?? {})
        : [],
    [schemaAvailable, inputSchema, schemaDefinitions, parsedArgs],
  );

  // Default to the form the first time a renderable schema arrives with a JSON
  // object in the args. A ref guard makes this fire exactly once, so a later
  // manual tab choice is respected even as `parsedArgs` keeps changing.
  const autoSelectedFormRef = useRef(false);
  useEffect(() => {
    if (autoSelectedFormRef.current) return;
    if (schemaAvailable && parsedArgs !== null) {
      autoSelectedFormRef.current = true;
      setArgsMode("form");
    }
  }, [schemaAvailable, parsedArgs]);

  // Form edits stringify straight back into the same `argsJson` state — no
  // second copy of state, so the JSON tab always shows the live form value.
  const handleFormChange = (next: Record<string, unknown>): void => {
    setArgsJson(JSON.stringify(next, null, 2));
    setArgsError(null);
  };

  const handleRun = async () => {
    if (!addressNoPrefix) return;

    const decoded = decodeArgsJson(argsJson);
    if (Option.isNone(decoded)) {
      setArgsError("Invalid JSON — enter a valid JSON object of arguments.");
      return;
    }
    const parsed = decoded.value;
    setArgsError(null);
    setRunning(true);
    setResult(null);

    // Read-only invoke: pass the validated args through as JSON. Empty
    // `reactivityKeys` — invoking a tool doesn't mutate `tools.list`.
    // `autoApprove` because the operator clicked Run: that IS the approval, so
    // an approval-gated tool should run here instead of dead-ending on a pause.
    const code = `return await tools[${JSON.stringify(addressNoPrefix)}](${JSON.stringify(parsed)});`;
    const exit = await doExecute({ payload: { code, autoApprove: true }, reactivityKeys: [] });
    setRunning(false);

    if (Exit.isFailure(exit)) {
      trackEvent("tool_run_submitted", {
        integration_slug: integration,
        tool_name: toolName,
        args_mode: argsMode,
        result: "failed",
      });
      toast.error("Failed to run tool");
      return;
    }

    const value = exit.value;
    if (value.status === "paused") {
      trackEvent("tool_run_submitted", {
        integration_slug: integration,
        tool_name: toolName,
        args_mode: argsMode,
        result: "paused",
      });
      setResult({ kind: "paused", text: value.text });
      return;
    }
    trackEvent("tool_run_submitted", {
      integration_slug: integration,
      tool_name: toolName,
      args_mode: argsMode,
      result: "completed",
      ...(value.isError ? { is_error: true } : {}),
    });
    setResult({
      kind: "completed",
      text: value.text,
      structured: value.structured,
      isError: value.isError,
    });
  };

  if (connections.length === 0) {
    return <p className="text-sm text-muted-foreground">Add a connection to test tools.</p>;
  }
  const renderConnectionPicker = shouldRenderConnectionPicker(connections);

  return (
    <div className="space-y-4">
      <CardStack>
        <CardStackContent className="space-y-4 px-4 py-4">
          {renderConnectionPicker && (
            <div className="space-y-1.5">
              <Label htmlFor="tool-run-connection">Connection</Label>
              <NativeSelect
                id="tool-run-connection"
                className="w-full"
                value={selectedConnection ?? ""}
                onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
                  setSelectedConnection(e.target.value as ConnectionName)
                }
                disabled={running}
              >
                {connections.map((connection: Connection) => (
                  <NativeSelectOption key={connection.name} value={connection.name}>
                    {connection.identityLabel || connection.name}
                  </NativeSelectOption>
                ))}
              </NativeSelect>
              <p className="text-xs text-muted-foreground">
                Pick the account whose credentials you want to verify.
              </p>
            </div>
          )}

          {/* Arguments editor — dynamic form built from the input JSON Schema,
              with a Raw JSON escape hatch. Both views share the `argsJson`
              string as the single source of truth. */}
          <div className="space-y-1.5">
            <Tabs
              value={argsMode}
              onValueChange={(v: string) => setArgsMode(v === "form" ? "form" : "json")}
            >
              <TabsList variant="line" className="w-full justify-start border-b">
                <TabsTrigger value="form" disabled={!schemaAvailable || parsedArgs === null}>
                  Form
                </TabsTrigger>
                <TabsTrigger value="json">Raw JSON</TabsTrigger>
              </TabsList>

              <TabsContent value="form" className="space-y-3 pt-3">
                {schemaAvailable && parsedArgs !== null ? (
                  <JsonSchemaForm
                    schema={inputSchema}
                    definitions={schemaDefinitions}
                    value={parsedArgs}
                    onChange={handleFormChange}
                    disabled={running}
                  />
                ) : (
                  <p className="text-xs text-muted-foreground">
                    No schema available — use Raw JSON to enter arguments.
                  </p>
                )}
              </TabsContent>

              <TabsContent value="json" className="space-y-1.5 pt-3">
                <Textarea
                  id="tool-run-args"
                  className="font-mono text-xs"
                  rows={5}
                  spellCheck={false}
                  value={argsJson}
                  onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
                    setArgsJson(e.target.value)
                  }
                  disabled={running}
                  aria-invalid={argsError != null}
                />
                {argsError && <p className="text-xs text-destructive">{argsError}</p>}
              </TabsContent>
            </Tabs>
          </div>

          <div className="flex items-center justify-between gap-3">
            {requiredMissing.length > 0 ? (
              <p className="text-xs text-muted-foreground">
                Fill required:{" "}
                <span className="font-mono text-foreground/80">{requiredMissing.join(", ")}</span>
              </p>
            ) : (
              <span aria-hidden />
            )}
            <Button
              size="sm"
              onClick={() => void handleRun()}
              disabled={running || requiredMissing.length > 0}
            >
              {running ? "Running..." : "Run"}
            </Button>
          </div>
        </CardStackContent>
      </CardStack>

      {result && <ToolRunResult result={result} />}
    </div>
  );
}

function ToolRunResult(props: { readonly result: RunResult }) {
  const { result } = props;

  if (result.kind === "paused") {
    return (
      <CardStack>
        <CardStackHeader>Result</CardStackHeader>
        <CardStackContent>
          <p className="px-4 py-3 text-sm text-muted-foreground">
            This tool requires approval (a policy gates it) — adjust the policy to run it directly.
          </p>
        </CardStackContent>
      </CardStack>
    );
  }

  const failed = result.isError || isFailedEnvelope(result.structured);
  const hasStructured = result.structured !== undefined;

  return (
    <CardStack className={cn(failed ? "border-destructive/40" : undefined)}>
      <CardStackHeader>
        <div className="flex items-center gap-2">
          <span>Result</span>
          <Badge variant={failed ? "destructive" : "secondary"}>
            {failed ? "Error" : "Success"}
          </Badge>
        </div>
      </CardStackHeader>
      <CardStackContent className="space-y-3 px-4 py-4">
        {failed && (
          <p className="text-xs text-destructive">
            The tool returned an error — the credentials may be invalid or lack access.
          </p>
        )}
        {hasStructured ? (
          <CodeBlock
            code={prettyJson(result.structured)}
            lang="json"
            title="Structured"
            className={cn(failed ? "border-destructive/40" : undefined)}
          />
        ) : null}
        {result.text && result.text.length > 0 && (
          <div className="space-y-1">
            <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
              Text
            </p>
            <pre className="max-h-48 overflow-auto rounded-md border border-border bg-card/60 p-3 font-mono text-xs whitespace-pre-wrap break-words text-foreground/80">
              {result.text}
            </pre>
          </div>
        )}
      </CardStackContent>
    </CardStack>
  );
}
