/**
 * File-editor visualizer for `file_editor` / `str_replace_editor` tools.
 *
 * Observation card: error → message; `str_replace`/`insert` → diff of the file
 * before vs after; `create`/`view` → the file content. Action card (shown while
 * the edit is in flight): `create` → new content; `str_replace` → diff of the
 * replaced snippet; `view`/`undo_edit` → just the path + range.
 *
 * Created Markdown artifacts get a height-clipped rich preview with a View bar
 * that opens the Files drawer, instead of dumping the full source into a code
 * block. Reads/edits of `.md` files keep the normal CodeBlock / DiffView path.
 */
import React from "react";
import { getLanguageFromPath } from "#/utils/get-language-from-path";
import { useOptionalConversationId } from "#/hooks/use-conversation-id";
import { openWorkspaceFile } from "#/services/canvas-ui";
import type {
  FileEditorAction,
  StrReplaceEditorAction,
} from "#/types/agent-server/core/base/action";
import type {
  FileEditorObservation,
  StrReplaceEditorObservation,
} from "#/types/agent-server/core/base/observation";
import { defineVisualizer, VisualizerProps } from "../define";
import { textFromContent } from "../text-content";
import { CodeBlock } from "../primitives/code-block";
import { DiffView } from "../primitives/diff-view";
import { FilePathChip } from "../primitives/file-path-chip";
import {
  isMarkdownFilePath,
  MarkdownFilePreview,
} from "../primitives/markdown-file-preview";

type FileEditorCardProps = VisualizerProps<
  FileEditorAction | StrReplaceEditorAction,
  FileEditorObservation | StrReplaceEditorObservation
>;

/** The path the event touched; observations win because they are authoritative. */
const resolvePath = ({ action, observation }: FileEditorCardProps): string =>
  observation?.observation.path ?? action?.action.path ?? "";

interface FileEditorCardBodyProps extends FileEditorCardProps {
  /**
   * Deep-links the file into the Files drawer. Absent outside a conversation
   * route, where there is no drawer to open; the card then stays read-only.
   * Also omitted for in-flight creates so View does not open a missing file.
   */
  onOpenFile?: () => void;
}

function FileEditorCardBody({
  action,
  observation,
  onOpenFile,
}: FileEditorCardBodyProps) {
  const path = resolvePath({ action, observation });
  const command = observation?.observation.command ?? action?.action.command;
  const language = getLanguageFromPath(path);

  const viewRange = action?.action.view_range;
  const range =
    command === "view" && viewRange
      ? `${viewRange[0]}-${viewRange[1]}`
      : undefined;
  const chip = path ? (
    <FilePathChip path={path} range={range} onClick={onOpenFile} />
  ) : null;

  const renderFileContent = (content: string) => {
    // Only created Markdown artifacts get the rich preview — `view` returns
    // `cat -n` numbered snippets that must stay in a CodeBlock.
    if (path && command === "create" && isMarkdownFilePath(path)) {
      // Markdown artifacts own their card (clipped preview + optional View),
      // so skip the separate path chip to avoid a duplicate filename affordance.
      return {
        chip: null as React.ReactNode,
        body: (
          <MarkdownFilePreview
            content={content}
            path={path}
            onView={onOpenFile}
          />
        ),
      };
    }
    return {
      chip,
      body: <CodeBlock code={content} language={language} />,
    };
  };

  if (observation) {
    const obs = observation.observation;
    let body: React.ReactNode = null;
    let leadingChip: React.ReactNode = chip;
    if (obs.error) {
      body = (
        <span className="whitespace-pre-wrap text-xs text-danger">
          {obs.error}
        </span>
      );
    } else if (obs.old_content != null && obs.new_content != null) {
      // Nullish, not truthy: an empty string is a valid side of the diff —
      // clearing a file or inserting into an empty file must still render it.
      body = <DiffView oldText={obs.old_content} newText={obs.new_content} />;
    } else {
      // `view` returns the snippet the agent saw in `content` (the `cat -n`
      // output) rather than `output`/`new_content`, so fall back to it.
      // Mirrors the markdown path's "prefer content for view" handling.
      const content =
        obs.new_content ||
        obs.output ||
        (obs.content ? textFromContent(obs.content) : "");
      if (content) {
        const rendered = renderFileContent(content);
        leadingChip = rendered.chip;
        body = rendered.body;
      }
    }
    return (
      <div className="flex flex-col gap-2">
        {leadingChip}
        {body}
      </div>
    );
  }

  if (action) {
    const act = action.action;
    let body: React.ReactNode = null;
    let leadingChip: React.ReactNode = chip;
    if (act.command === "create" && act.file_text) {
      const rendered = renderFileContent(act.file_text);
      leadingChip = rendered.chip;
      body = rendered.body;
    } else if (
      (act.command === "str_replace" || act.command === "insert") &&
      act.new_str != null
    ) {
      // `insert` carries only `new_str`, no `old_str`. Key on `new_str` and
      // default `old_str` to "" so an in-flight insert shows an addition diff
      // instead of nothing.
      body = <DiffView oldText={act.old_str ?? ""} newText={act.new_str} />;
    }
    return (
      <div className="flex flex-col gap-2">
        {leadingChip}
        {body}
      </div>
    );
  }

  return null;
}

/**
 * Conversation-scoped wrapper: owns the Files-drawer deep link. Split from the
 * card so conversation-only navigation never runs outside a conversation route.
 */
function FileEditorCardWithDrawerLink({
  conversationId,
  ...props
}: FileEditorCardProps & { conversationId: string }) {
  const path = resolvePath(props);

  // Only deep-link once the observation exists — the file is not guaranteed
  // to be on disk while the create action is still in flight.
  const onOpenFile =
    path && props.observation
      ? () => openWorkspaceFile(path, conversationId)
      : undefined;

  return <FileEditorCardBody {...props} onOpenFile={onOpenFile} />;
}

export const fileEditorVisualizer = defineVisualizer({
  actionKinds: ["FileEditorAction", "StrReplaceEditorAction"],
  observationKinds: ["FileEditorObservation", "StrReplaceEditorObservation"],
  Body: function FileEditorBody(props) {
    const { conversationId } = useOptionalConversationId();

    return conversationId ? (
      <FileEditorCardWithDrawerLink
        {...props}
        conversationId={conversationId}
      />
    ) : (
      <FileEditorCardBody {...props} />
    );
  },
});
