import { useEffect, useMemo, useRef, useState } from "react";
import { Check, Paperclip, RefreshCw, Search, Sparkles, X } from "lucide-react";
import {
  chat,
  type ChatAttachment,
  type ChatUsage,
  listWorkspaceFiles,
  onChatStream,
  readWorkspaceFile as readWorkspaceFile,
} from "../lib/wailsBackend";
import {
  chatModelChoices,
  type ChatProvider,
  type ChatSettings,
  cliNames,
  type CLIType,
  configuredChatProviders,
  providerDefaults,
  settingsForModel,
  switchChatProvider,
} from "../llm/settings";
import {
  findWorkflowBlocks,
  parseWorkflowFromMarkdown,
  serializeWorkflowData,
} from "./parser";
import type { WorkflowRun } from "./types";
import { buildWorkflowGenerationSpec } from "./workflowSpec";
import yaml from "js-yaml";
import { UnifiedDiff } from "../components/UnifiedDiff";

type Phase =
  | "input"
  | "external"
  | "planning"
  | "plan-review"
  | "generating"
  | "reviewing"
  | "result";
interface ReviewIssue {
  severity?: string;
  nodeId?: string;
  message?: string;
  suggestion?: string;
}
interface ReviewResult {
  verdict: "pass" | "fail";
  summary: string;
  issues: ReviewIssue[];
}

function extractSkillInstructions(text: string): string {
  return text.match(/```skill-instructions\s*\r?\n([\s\S]*?)\r?\n```/i)?.[1]
    .trim() || "";
}

function externalWorkflowBlock(text: string) {
  const direct = findWorkflowBlocks(text)[0];
  if (direct) return direct;
  for (
    const match of text.matchAll(/```(?:ya?ml)?\s*\r?\n([\s\S]*?)\r?\n```/gi)
  ) {
    const block = findWorkflowBlocks(match[1].trim())[0];
    if (block) return block;
  }
  return undefined;
}

export function parseExternalWorkflowResponse(
  text: string,
  artifactKind: "skill" | "workflow",
) {
  const block = externalWorkflowBlock(text);
  let instructions = artifactKind === "skill"
    ? extractSkillInstructions(text)
    : "";
  if (artifactKind === "skill" && !instructions && block) {
    const fence = text.search(/^`{3,}(?:hub-workflow|workflow|ya?ml)?\s*$/im);
    if (fence > 0) instructions = text.slice(0, fence).trim();
  }
  return { block, instructions };
}

export function parseReviewResponse(text: string): ReviewResult | null {
  const trimmed = text.trim();
  const fenced = [...trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)].map((
    match,
  ) => match[1].trim());
  const firstBrace = trimmed.indexOf("{");
  const lastBrace = trimmed.lastIndexOf("}");
  const candidates = [
    trimmed,
    ...fenced,
    firstBrace >= 0 && lastBrace > firstBrace
      ? trimmed.slice(firstBrace, lastBrace + 1)
      : "",
  ].filter(Boolean);
  for (const candidate of candidates) {
    try {
      const value = JSON.parse(candidate) as Partial<ReviewResult>;
      if (value.verdict !== "pass" && value.verdict !== "fail") continue;
      const issues = Array.isArray(value.issues)
        ? value.issues.filter((issue): issue is ReviewIssue =>
          !!issue && typeof issue.message === "string"
        )
        : [];
      return {
        verdict: value.verdict === "fail" ||
            issues.some((issue) => issue.severity === "high")
          ? "fail"
          : "pass",
        summary: typeof value.summary === "string" && value.summary.trim()
          ? value.summary.trim()
          : value.verdict === "pass"
          ? "Review passed."
          : "Review found issues.",
        issues,
      };
    } catch {
      // Try the next possible JSON section. Some providers wrap JSON in prose.
    }
  }
  return null;
}

function mergeUsage(
  total: ChatUsage | undefined,
  next: ChatUsage | undefined,
): ChatUsage | undefined {
  if (!next) return total;
  return {
    inputTokens: (total?.inputTokens || 0) + (next.inputTokens || 0),
    outputTokens: (total?.outputTokens || 0) + (next.outputTokens || 0),
    thinkingTokens: (total?.thinkingTokens || 0) + (next.thinkingTokens || 0),
    totalTokens: (total?.totalTokens || 0) + (next.totalTokens || 0),
    cachedTokens: (total?.cachedTokens || 0) + (next.cachedTokens || 0),
  };
}

function withoutFrontmatter(content: string): string {
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
}

export function outputPathForArtifactName(
  kind: "skill" | "workflow",
  name: string,
): string {
  const fallback = kind === "skill" ? "new-skill" : "new-workflow";
  const slug = name
    .normalize("NFKD")
    .toLocaleLowerCase()
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^\p{L}\p{N}._-]+/gu, "-")
    .replace(/^[-._]+|[-._]+$/g, "") || fallback;
  return kind === "skill"
    ? `skills/${slug}`
    : `workflows/${slug}.workflow.yaml`;
}

export function workflowBuilderModelOptions(
  settings: ChatSettings,
  provider: ChatProvider,
): Array<{ value: string; label: string }> {
  if (provider === "cli") {
    return settings.verifiedCliTypes.map((type) => ({
      value: type,
      label: cliNames[type],
    }));
  }
  return [
    ...new Set([
      ...settings.modelProfiles.filter((item) =>
        item.provider === provider && item.enabled
      ).flatMap((item) => item.enabledModels),
      ...chatModelChoices[provider],
    ]),
  ].filter(Boolean).map((value) => ({ value, label: value }));
}

export function workflowGenerationContext(
  settings: ChatSettings,
  selectedModel = settings.model,
) {
  return {
    models: [
      ...new Set([
        selectedModel,
        settings.model,
        ...settings.modelProfiles.filter((profile) => profile.enabled).flatMap(
          (profile) => profile.enabledModels,
        ),
      ].filter(Boolean)),
    ],
    ragSettings: Object.keys(settings.ragSettings),
    mcpServers: settings.mcpServers.filter((server) => server.enabled).map(
      (server) => server.name,
    ),
  };
}

export function buildWorkflowReviewSystemPrompt(spec: string): string {
  return `You are a precise GemiHub Desktop workflow quality reviewer. Evaluate the complete workflow primarily against the user's original request. The approved plan is implementation guidance, not an independent source of new requirements.

Requirement priority and severity:
- Treat behavior explicitly requested by the user or additional requirements as mandatory.
- Use the approved plan to understand the intended implementation, but do not turn optional suggestions, illustrative edge cases, or planner-invented features into mandatory requirements.
- Do not require retry loops, alternate URL prompts, pasted-content fallbacks, multiple output formats, or extra dialogs unless the original request explicitly asks for them.
- Do not require explicit empty-value checks, HTTP status branches, URL normalization scripts, or custom error-result nodes for ordinary happy-path workflows. Runtime failures may surface directly.
- Accept a simple command with __websearch__ as the preferred way to read and transform a public webpage; do not demand raw HTTP fetching or HTML extraction scripts.
- Gracefully ending with a clear error result is valid handling for an inaccessible external resource unless recovery behavior was explicitly requested.
- High severity is reserved for a guaranteed runtime failure, unsafe data loss, or failure to deliver behavior explicitly requested by the user. A quality improvement or unrequested enhancement is medium/low and must not cause a fail verdict.
- If the workflow itself asks the user for an option but ignores that option, report the inconsistency. Prefer suggesting removal of the unused option when the original request did not require it.
- Report an issue only when changing the workflow is necessary to satisfy an explicit request or prevent a concrete, likely execution failure.
- Do not report speculative robustness concerns based on hypothetical provider behavior, rare network states, alternative wording, cosmetic distinctions, or features that merely could be added.
- Omit low-severity polish entirely. Use medium only for a concrete output mismatch that a typical run can reach. If the main request is fulfilled and the graph is structurally executable, return pass with an empty issues array.

Check all of the following:
1. Completeness: every requested behavior, input, output, and edge case is implemented.
2. Graph correctness: node types exist, IDs are unique, targets exist, only while nodes receive backward edges, and all branches terminate.
3. Data flow: every referenced variable is initialized by caller input, variable/set, a system variable, or a save property before use; output-producing nodes use saveTo where needed.
4. Interactive versus headless execution: standalone workflows prompt for required values; skill, event, hotkey, and dashboard workflows do not depend on unavailable dialogs or pickers. Skills return meaningful non-underscore variables instead of displaying a final dialog.
5. JavaScript interpolation: {{var:json}} used as a string has surrounding quotes. Flag bare uses such as const value = {{var:json}}.
6. JSON nodes: json.source is exactly a bare variable name, never {{var}}, a quoted interpolation, or wrapped JSON.
7. HTTP and shell errors: throwOnError remains true unless status/exit code is explicitly saved and handled by a downstream branch. Returning an error-looking string is not error handling.
8. File safety: writes use the correct Desktop note/file nodes and confirmation unless the request explicitly requires unattended execution.
9. Runtime configuration: model, RAG, and MCP names exist in the supplied configuration; do not accept invented names.

Return exactly one JSON object with no Markdown fences:
{"verdict":"pass|fail","summary":"...","issues":[{"severity":"high|medium|low","nodeId":"...","message":"...","suggestion":"..."}]}
Use fail only when a high-severity issue meets the calibration above. Do not manufacture suggestions to make the review look thorough. A clean pass with no issues is preferred when the requested workflow should work. Do not repeat the workflow YAML.

AUTHORITATIVE DESKTOP SPECIFICATION:
${spec}`;
}

export function buildWorkflowPlanSystemPrompt(): string {
  return "Plan the shortest practical Workspace workflow in plain language. Cover the goal, only the necessary ordered steps, required inputs, and outputs. Prefer direct high-level operations; for a public webpage, one AI step can read and transform the URL. Do not invent validation stages, optional input fields, output formats, retry loops, fallback interactions, or other features the user did not request. Runtime failures may surface directly instead of requiring custom recovery steps. Do not mention YAML or node types. Keep it concise.";
}

export function buildWorkflowRefinementPrompt(options: {
  request: string;
  plan: string;
  previous: string;
  review: ReviewResult | null;
  feedback: string;
  artifactKind: "skill" | "workflow";
}): string {
  const issues =
    options.review?.issues.map((issue) =>
      `- [${issue.severity || "issue"}]${
        issue.nodeId ? ` ${issue.nodeId}:` : ""
      } ${issue.message || ""}${
        issue.suggestion ? ` Fix: ${issue.suggestion}` : ""
      }`
    ).join("\n") ||
    "- No structured issues were returned; perform a full correctness audit.";
  return `Repair the previously generated ${options.artifactKind}. Preserve behavior that already satisfies the request and return the complete corrected artifact, never a partial patch.

ORIGINAL REQUEST:
${options.request}
${options.plan ? `\nAPPROVED PLAN:\n${options.plan}\n` : ""}
REVIEW SUMMARY:
${
    options.review?.summary ||
    "Review could not be parsed and must be treated as a failure."
  }

REVIEW ISSUES:
${issues}
${options.feedback ? `\nUSER REFINEMENT:\n${options.feedback}\n` : ""}
PREVIOUS GENERATED ${options.artifactKind.toUpperCase()}:
${options.previous}

Fix every high-severity issue, re-check variable initialization and every connection, and obey the output contract from the system specification.`;
}

export function invalidWorkflowReview(): ReviewResult {
  return {
    verdict: "fail",
    summary:
      "The AI review could not be parsed, so the workflow was not accepted as verified.",
    issues: [{
      severity: "high",
      message: "The reviewer did not return the required structured result.",
      suggestion: "Refine the workflow to run a fresh correctness review.",
    }],
  };
}

export function AIWorkflowBuilderModal({
  mode,
  artifactKind = "workflow",
  currentMarkdown,
  currentPath,
  currentName,
  activeFile,
  settings,
  history,
  additionalInstructions = "",
  onApply,
  onClose,
}: {
  mode: "create" | "modify";
  artifactKind?: "workflow" | "skill";
  currentMarkdown: string;
  currentPath: string;
  currentName?: string;
  activeFile?: { path: string; content: string } | null;
  settings: ChatSettings;
  history: WorkflowRun[];
  additionalInstructions?: string;
  onApply: (
    result: {
      block: string;
      path: string;
      name: string;
      request: string;
      skillInstructions?: string;
    },
  ) => Promise<void>;
  onClose: () => void;
}) {
  const [phase, setPhase] = useState<Phase>("input");
  const [provider, setProvider] = useState<ChatProvider>(settings.provider);
  const [model, setModel] = useState(settings.model);
  const [cliType, setCLIType] = useState<CLIType>(settings.cliType);
  const [name, setName] = useState(
    mode === "modify"
      ? currentName || currentPath.split("/").pop()?.replace(/\.md$/i, "") ||
        "Workflow"
      : artifactKind === "skill"
      ? "New Skill"
      : "New Workflow",
  );
  const [path, setPath] = useState(
    mode === "modify"
      ? currentPath
      : artifactKind === "skill"
      ? "skills/new-skill"
      : "workflows/new-workflow.workflow.yaml",
  );
  const [request, setRequest] = useState("");
  const [plan, setPlan] = useState("");
  const [planFeedback, setPlanFeedback] = useState("");
  const [generatedBlock, setGeneratedBlock] = useState("");
  const [skillInstructions, setSkillInstructions] = useState("");
  const [review, setReview] = useState<ReviewResult | null>(null);
  const [refineFeedback, setRefineFeedback] = useState("");
  const [externalResponse, setExternalResponse] = useState("");
  const [externalPrompt, setExternalPrompt] = useState("");
  const [attachmentPaths, setAttachmentPaths] = useState<string[]>([]);
  const [attachmentPickerOpen, setAttachmentPickerOpen] = useState(false);
  const [attachmentChoices, setAttachmentChoices] = useState<string[]>([]);
  const [attachmentQuery, setAttachmentQuery] = useState("");
  const [attachmentLoading, setAttachmentLoading] = useState(false);
  const [acceptReviewIssues, setAcceptReviewIssues] = useState(false);
  const [thinking, setThinking] = useState("");
  const [streamText, setStreamText] = useState("");
  const [error, setError] = useState("");
  const [usage, setUsage] = useState<ChatUsage>();
  const [elapsedMs, setElapsedMs] = useState(0);
  const [selectedRuns, setSelectedRuns] = useState<string[]>([]);
  const [selectedSteps, setSelectedSteps] = useState<Record<string, string[]>>(
    {},
  );
  const authoringSpec = useMemo(
    () =>
      buildWorkflowGenerationSpec(workflowGenerationContext(settings, model)),
    [settings, model],
  );
  const streamRef = useRef("");
  const requestRef = useRef<HTMLTextAreaElement>(null);
  const configured = configuredChatProviders(settings);
  const relevantHistory = useMemo(
    () =>
      history.filter((run) => !currentPath || run.workflowPath === currentPath)
        .slice(0, 10),
    [currentPath, history],
  );
  const filteredAttachmentChoices = useMemo(() => {
    const query = attachmentQuery.trim().toLocaleLowerCase();
    return attachmentChoices.filter((filePath) =>
      !attachmentPaths.includes(filePath) &&
      (!query || filePath.toLocaleLowerCase().includes(query))
    ).slice(0, 100);
  }, [attachmentChoices, attachmentPaths, attachmentQuery]);
  useEffect(() =>
    onChatStream((event) => {
      if (event.streamId !== streamRef.current) return;
      if (event.type === "thinking" && event.delta) {
        setThinking((value) => value + event.delta);
      }
      if (event.type === "text" && event.delta) {
        setStreamText((value) => value + event.delta);
      }
    }), []);

  useEffect(() => {
    if (mode === "create") {
      setPath(outputPathForArtifactName(artifactKind, name));
    }
  }, [artifactKind, mode, name]);

  const providerSettings = () => {
    const resolved = provider === settings.provider
      ? settings
      : switchChatProvider(settings, provider);
    const selected = settingsForModel(resolved, model);
    return provider === "cli" ? { ...selected, cliType } : selected;
  };
  const openAttachmentPicker = async () => {
    setAttachmentPickerOpen((open) => !open);
    if (attachmentChoices.length || attachmentLoading) return;
    setAttachmentLoading(true);
    try {
      const entries = await listWorkspaceFiles();
      setAttachmentChoices(entries.map((entry) => entry.path));
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
    } finally {
      setAttachmentLoading(false);
    }
  };
  const openExternal = async () => {
    if (!name.trim()) {
      setError("Enter a workflow name before using an external LLM.");
      return;
    }
    if (!request.trim()) {
      setError(
        "Describe the workflow change in Request before using an external LLM.",
      );
      requestRef.current?.focus();
      return;
    }
    setError("");
    try {
      const references = await referenceInputs();
      const historyText = historyContext();
      const outputContract = artifactKind === "skill"
        ? "Return exactly two fenced blocks and no other text: first a `skill-instructions` Markdown block, then one complete `hub-workflow` YAML block."
        : "Return exactly one complete `hub-workflow` fenced YAML block and no other text.";
      setExternalPrompt(
        `You are a workflow author for GemiHub Desktop. Follow the complete specification below.\n\n${authoringSpec}\n\nOUTPUT CONTRACT:\n${outputContract}\n\n${
          mode === "create" ? "Create" : "Modify"
        } the ${artifactKind} named "${name}".\n\nUSER REQUEST:\n${request}${
          additionalInstructions
            ? `\n\nADDITIONAL REQUIREMENTS:\n${additionalInstructions}`
            : ""
        }${plan ? `\n\nAPPROVED PLAN:\n${plan}` : ""}${
          mode === "modify"
            ? `\n\nCURRENT ${artifactKind.toUpperCase()}:\n${currentMarkdown}`
            : ""
        }${references.text ? `\n\nREFERENCED FILES:\n${references.text}` : ""}${
          references.attachments.length
            ? `\n\nBINARY ATTACHMENTS NOT EMBEDDED IN THIS TEXT PROMPT:\n${
              references.attachments.map((item) => `- ${item.name}`).join("\n")
            }\nAttach these files separately if they are required.`
            : ""
        }${
          historyText ? `\n\nSELECTED EXECUTION HISTORY:\n${historyText}` : ""
        }`,
      );
      setPhase("external");
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
    }
  };
  const call = async (
    systemPrompt: string,
    userPrompt: string,
    attachments: ChatAttachment[] = [],
  ) => {
    const resolved = providerSettings(),
      streamId = crypto.randomUUID(),
      started = performance.now();
    streamRef.current = streamId;
    setThinking("");
    setStreamText("");
    setError("");
    // Workflow authoring/review always enables reasoning, matching the source
    // workflow generator independently of the normal chat toggle.
    const result = await chat({
      provider: resolved.provider,
      endpoint: resolved.endpoint,
      apiKey: resolved.apiKey,
      localFramework: resolved.localFramework,
      localUsername: resolved.localUsername,
      localPassword: resolved.localPassword,
      model: resolved.provider === "cli" ? "" : resolved.model,
      vertexProjectId: resolved.vertexProjectId,
      vertexLocation: resolved.vertexLocation,
      systemPrompt,
      messages: [{
        role: "user",
        content: userPrompt,
        attachments: attachments.length ? attachments : undefined,
      }],
      enableFileTools: false,
      fileToolMode: "none",
      cliType: resolved.cliType,
      cliPath: resolved.cliPaths[resolved.cliType],
      cliSessionId: "",
      streamId,
      enableThinking: true,
    });
    setUsage((value) => mergeUsage(value, result.usage));
    setElapsedMs((value) => value + Math.round(performance.now() - started));
    streamRef.current = "";
    return result.content;
  };

  const historyContext = () =>
    selectedRuns.map((id) => relevantHistory.find((run) => run.id === id))
      .filter(Boolean).map((run) => {
        const keys = new Set(selectedSteps[run!.id] ?? []);
        const logs = run!.logs.filter((log, index) =>
          log.status !== "info" && keys.has(`${log.nodeId}:${index}`)
        );
        return `Run ${run!.startTime} (${run!.status})\n${
          logs.map((log) =>
            `${log.nodeId}/${log.nodeType}: ${log.status} ${log.message}\nInput: ${
              JSON.stringify(log.input)
            }\nOutput: ${JSON.stringify(log.output)}${
              log.usage ? `\nUsage: ${JSON.stringify(log.usage)}` : ""
            }`
          ).join("\n")
        }`;
      }).join("\n\n");

  const toggleHistoryRun = (run: WorkflowRun, checked: boolean) => {
    setSelectedRuns((items) =>
      checked
        ? [...new Set([...items, run.id])]
        : items.filter((id) => id !== run.id)
    );
    setSelectedSteps((current) => {
      const next = { ...current };
      if (checked) {
        next[run.id] = run.logs.map((log, index) => ({ log, index })).filter((
          { log },
        ) => log.status !== "info").map(({ log, index }) =>
          `${log.nodeId}:${index}`
        );
      } else delete next[run.id];
      return next;
    });
  };

  const toggleHistoryStep = (
    run: WorkflowRun,
    key: string,
    checked: boolean,
  ) => {
    setSelectedSteps((current) => ({
      ...current,
      [run.id]: checked
        ? [...new Set([...(current[run.id] ?? []), key])]
        : (current[run.id] ?? []).filter((value) => value !== key),
    }));
  };

  const referenceInputs = async () => {
    const references = new Map<string, string>();
    const attachments: ChatAttachment[] = [];
    if (activeFile && /@\{(?:selection|content)\}/.test(request)) {
      references.set(activeFile.path, activeFile.content);
    }
    const paths = new Set(
      [...request.matchAll(/@([\w./-]+\.[A-Za-z0-9_-]+)/g)].map((match) =>
        match[1]
      ),
    );
    for (
      const path of attachmentPaths
    ) paths.add(path);
    for (const path of paths) {
      const file = await readWorkspaceFile(path).catch(() => null);
      if (!file) continue;
      const dataUrl = file.content.match(/^data:([^;,]+);base64,([\s\S]+)$/);
      if (dataUrl) {
        attachments.push({
          name: file.fileName || path.split("/").pop() || path,
          mimeType: dataUrl[1],
          data: dataUrl[2],
        });
      } else references.set(path, file.content);
    }
    return {
      text: [...references].map(([filePath, content]) =>
        `--- BEGIN REFERENCE: ${filePath} ---\n${
          withoutFrontmatter(content).slice(0, 200_000)
        }\n--- END REFERENCE ---`
      ).join("\n\n"),
      attachments,
    };
  };

  const runPlanning = async (feedback = "") => {
    setPhase("planning");
    try {
      const references = await referenceInputs();
      const text = await call(
        buildWorkflowPlanSystemPrompt(),
        `Workflow name: ${name}\nRequest: ${request}${
          additionalInstructions
            ? `\nAdditional requirements:\n${additionalInstructions}`
            : ""
        }${references.text ? `\n\nReferenced files:\n${references.text}` : ""}${
          feedback ? `\nFeedback on previous plan: ${feedback}` : ""
        }`,
        references.attachments,
      );
      setPlan(text);
      setPhase("plan-review");
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
      setPhase("input");
    }
  };

  const generate = async (feedback = "") => {
    setPhase("generating");
    setReview(null);
    setAcceptReviewIssues(false);
    const historyText = historyContext();
    const references = await referenceInputs();
    const completeRequest = `${request}${
      additionalInstructions
        ? `\n\nAdditional requirements:\n${additionalInstructions}`
        : ""
    }`;
    const basePrompt = `${
      mode === "create" ? "Create" : "Modify"
    } the ${artifactKind} named "${name}".\n\nUser request:\n${request}\n${
      additionalInstructions
        ? `\nAdditional requirements:\n${additionalInstructions}\n`
        : ""
    }${references.text ? `\nReferenced files:\n${references.text}\n` : ""}${
      plan ? `\nApproved plan:\n${plan}\n` : ""
    }${
      mode === "modify"
        ? `\nCurrent ${artifactKind}:\n${currentMarkdown}\n`
        : ""
    }${historyText ? `\nSelected execution history:\n${historyText}\n` : ""}`;
    try {
      const skillSpec = artifactKind === "skill"
        ? "\nYou are authoring an Agent Skill. Return exactly two fenced blocks: first `skill-instructions` containing concise Markdown instructions for the chat model, then one `hub-workflow` block. Do not put frontmatter or skill-capabilities in the instructions block. The workflow must save meaningful results to variables; the caller automatically receives them."
        : "";
      const previousArtifact = `${
        skillInstructions
          ? `\`\`\`skill-instructions\n${skillInstructions}\n\`\`\`\n`
          : ""
      }${generatedBlock}`;
      const generationPrompt = feedback
        ? buildWorkflowRefinementPrompt({
          request: completeRequest,
          plan,
          previous: previousArtifact,
          review,
          feedback,
          artifactKind,
        })
        : basePrompt;
      let response = await call(
        `${
          feedback
            ? "You repair a GemiHub Desktop workflow after a quality review."
            : "You are a GemiHub Desktop workflow author."
        } ${authoringSpec}${skillSpec}`,
        generationPrompt,
        references.attachments,
      );
      let block = findWorkflowBlocks(response)[0];
      let instructions = artifactKind === "skill"
        ? extractSkillInstructions(response)
        : "";
      for (
        let attempt = 1;
        (!block || block.error ||
          (artifactKind === "skill" && !instructions)) && attempt <= 2;
        attempt++
      ) {
        response = await call(
          `You repair invalid GemiHub Desktop workflow output. ${authoringSpec}${skillSpec}`,
          `The previous output is invalid. ${
            artifactKind === "skill"
              ? "Return one skill-instructions block followed by one complete valid hub-workflow block."
              : "Return one complete valid hub-workflow block only."
          }\n\nPrevious output:\n${response}\n\nError:\n${
            block?.error || (!instructions && artifactKind === "skill"
              ? "No skill-instructions block found"
              : "No workflow block found")
          }`,
          references.attachments,
        );
        block = findWorkflowBlocks(response)[0];
        instructions = artifactKind === "skill"
          ? extractSkillInstructions(response)
          : "";
      }
      if (!block || block.error) {
        throw new Error(
          block?.error || "AI response did not contain a valid workflow block.",
        );
      }
      if (artifactKind === "skill" && !instructions) {
        throw new Error(
          "AI response did not contain a skill-instructions block.",
        );
      }
      parseWorkflowFromMarkdown(block.raw);
      setGeneratedBlock(block.raw);
      setSkillInstructions(instructions);
      await reviewGenerated(
        `${
          instructions ? `Skill instructions:\n${instructions}\n\n` : ""
        }${block.raw}`,
        basePrompt,
      );
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
      setPhase(plan ? "plan-review" : "input");
    }
  };

  const reviewGenerated = async (block: string, originalRequest: string) => {
    setPhase("reviewing");
    const systemPrompt = buildWorkflowReviewSystemPrompt(authoringSpec);
    const prompt =
      `ORIGINAL REQUEST AND CONTEXT:\n${originalRequest}\n\nWORKFLOW TO REVIEW:\n${block}`;
    let response = await call(systemPrompt, prompt);
    let parsed = parseReviewResponse(response);
    if (!parsed) {
      response = await call(
        "Convert the supplied review response to the required JSON schema. Return JSON only; do not include or repeat any workflow YAML.",
        `Required schema: {\"verdict\":\"pass|fail\",\"summary\":\"...\",\"issues\":[]}\n\nInvalid review response:\n${
          response.slice(0, 20_000)
        }`,
      );
      parsed = parseReviewResponse(response);
    }
    setReview(parsed ?? invalidWorkflowReview());
    setPhase("result");
  };

  const begin = () => {
    if (!request.trim() || !name.trim()) return;
    if (mode === "create") void runPlanning();
    else void generate();
  };
  const acceptExternal = () => {
    try {
      let { block, instructions } = parseExternalWorkflowResponse(
        externalResponse,
        artifactKind,
      );
      if (!block && artifactKind === "workflow") {
        const parsed = yaml.load(externalResponse, {
          schema: yaml.JSON_SCHEMA,
        });
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
          block = findWorkflowBlocks(
            serializeWorkflowData(parsed as Record<string, unknown>),
          )[0];
        }
      }
      if (!block || block.error) {
        throw new Error(
          block?.error || "No workflow block was found in the pasted response.",
        );
      }
      if (artifactKind === "skill" && !instructions) {
        throw new Error(
          "No skill-instructions block was found in the pasted response.",
        );
      }
      parseWorkflowFromMarkdown(block.raw);
      setGeneratedBlock(block.raw);
      setSkillInstructions(instructions);
      setReview({
        verdict: "pass",
        summary:
          "The external response passed local YAML, node, and connection validation. AI review was not run.",
        issues: [],
      });
      setPhase("result");
      setError("");
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
    }
  };
  const apply = async () => {
    try {
      await onApply({
        block: generatedBlock,
        path: artifactKind === "skill"
          ? path.replace(/\/SKILL\.md$/i, "")
          : path,
        name,
        request,
        skillInstructions: artifactKind === "skill"
          ? skillInstructions
          : undefined,
      });
      onClose();
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : String(caught));
    }
  };
  const busy = ["planning", "generating", "reviewing"].includes(phase);
  return (
    <div className="workflow-modal-backdrop">
      <section className="ai-workflow-builder">
        <header>
          <div>
            <Sparkles size={18} />
            <strong>
              {mode === "create"
                ? `Create ${
                  artifactKind === "skill" ? "Skill" : "Workflow"
                } with AI`
                : `Modify ${
                  artifactKind === "skill" ? "Skill" : "Workflow"
                } with AI`}
            </strong>
          </div>
          {!busy && (
            <button type="button" onClick={onClose}>
              <X size={16} />
            </button>
          )}
        </header>
        {phase === "input" && (
          <div className="ai-workflow-input">
            <div className="ai-workflow-row">
              <label>
                <span>Provider</span>
                <select
                  value={provider}
                  onChange={(event) => {
                    const next = event.target.value as ChatProvider;
                    setProvider(next);
                    setModel(providerDefaults(next).model);
                    if (next === "cli") {
                      setCLIType((current) =>
                        settings.verifiedCliTypes.includes(current)
                          ? current
                          : settings.verifiedCliTypes[0] ?? current
                      );
                    }
                  }}
                >
                  {configured.map((value) => (
                    <option key={value} value={value}>{value}</option>
                  ))}
                </select>
              </label>
              <label>
                <span>Model</span>
                <select
                  value={provider === "cli" ? cliType : model}
                  onChange={(event) => {
                    if (provider === "cli") {
                      setCLIType(event.target.value as CLIType);
                    } else setModel(event.target.value);
                  }}
                >
                  {workflowBuilderModelOptions(settings, provider).map((
                    option,
                  ) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
                </select>
              </label>
            </div>
            <label>
              <span>Name</span>
              <input
                value={name}
                onChange={(event) => setName(event.target.value)}
              />
            </label>
            {mode === "create" && (
              <label>
                <span>Output path</span>
                <input
                  value={path}
                  onChange={(event) => setPath(event.target.value)}
                />
              </label>
            )}
            <label>
              <span>Request</span>
              <textarea
                ref={requestRef}
                rows={7}
                value={request}
                onChange={(event) => {
                  setRequest(event.target.value);
                  if (error) setError("");
                }}
              />
            </label>
            <div className="ai-workflow-attachments">
              <span>Reference attachments</span>
              <div className="ai-workflow-attachment-list">
                {attachmentPaths.map((filePath) => (
                  <span key={filePath}>
                    {filePath}
                    <button
                      type="button"
                      title={`Remove ${filePath}`}
                      onClick={() =>
                        setAttachmentPaths((paths) =>
                          paths.filter((path) => path !== filePath)
                        )}
                    >
                      <X size={12} />
                    </button>
                  </span>
                ))}
                <button
                  type="button"
                  onClick={() => void openAttachmentPicker()}
                >
                  <Paperclip size={13} /> Add file
                </button>
              </div>
              {attachmentPickerOpen && (
                <div className="ai-workflow-attachment-picker">
                  <label>
                    <Search size={13} />
                    <input
                      autoFocus
                      value={attachmentQuery}
                      onChange={(event) =>
                        setAttachmentQuery(event.target.value)}
                      placeholder="Find a Workspace file"
                    />
                  </label>
                  <div>
                    {attachmentLoading
                      ? <small>Loading files…</small>
                      : filteredAttachmentChoices.length
                      ? filteredAttachmentChoices.map((filePath) => (
                        <button
                          type="button"
                          key={filePath}
                          onClick={() => {
                            setAttachmentPaths((paths) => [...paths, filePath]);
                            setAttachmentQuery("");
                          }}
                        >
                          {filePath}
                        </button>
                      ))
                      : <small>No matching files</small>}
                  </div>
                </div>
              )}
              <small>
                Text is embedded; binary files must be attached separately when
                using an external LLM.
              </small>
            </div>
            {mode === "modify" && relevantHistory.length > 0 && (
              <fieldset className="ai-workflow-history-select">
                <legend>Reference execution history</legend>
                {relevantHistory.map((run) => (
                  <details key={run.id} open={selectedRuns.includes(run.id)}>
                    <summary>
                      <label
                        onClick={(event) =>
                          event.stopPropagation()}
                      >
                        <input
                          type="checkbox"
                          checked={selectedRuns.includes(run.id)}
                          onChange={(event) =>
                            toggleHistoryRun(run, event.target.checked)}
                        />
                        <span>
                          {new Date(run.startTime).toLocaleString()} ·{" "}
                          {run.status}
                        </span>
                      </label>
                    </summary>
                    {run.logs.map((log, index) => ({ log, index })).filter((
                      { log },
                    ) =>
                      log.status !== "info"
                    ).map(({ log, index }) => {
                      const key = `${log.nodeId}:${index}`;
                      return (
                        <label key={key}>
                          <input
                            type="checkbox"
                            checked={(selectedSteps[run.id] ?? []).includes(
                              key,
                            )}
                            disabled={!selectedRuns.includes(run.id)}
                            onChange={(event) =>
                              toggleHistoryStep(run, key, event.target.checked)}
                          />
                          <span>
                            <b>{log.nodeId} · {log.nodeType}</b>
                            <small>{log.status} · {log.message}</small>
                          </span>
                        </label>
                      );
                    })}
                  </details>
                ))}
              </fieldset>
            )}
            <footer>
              <button type="button" onClick={onClose}>Cancel</button>
              <button
                type="button"
                onClick={() => void openExternal()}
              >
                Use external LLM
              </button>
              <button
                type="button"
                className="primary"
                disabled={!request.trim() || !name.trim() ||
                  configured.length === 0}
                onClick={begin}
              >
                <Sparkles size={13} />Start
              </button>
            </footer>
          </div>
        )}
        {phase === "external" && (
          <div className="ai-workflow-review">
            <h3>External LLM</h3>
            <p>
              Copy this prompt to another model, then paste its complete
              response below.
            </p>
            <label>
              <span>Prompt</span>
              <textarea readOnly rows={8} value={externalPrompt} />
            </label>
            <button
              type="button"
              onClick={() => void navigator.clipboard.writeText(externalPrompt)}
            >
              Copy prompt
            </button>
            <label>
              <span>Response</span>
              <textarea
                rows={10}
                value={externalResponse}
                onChange={(event) => setExternalResponse(event.target.value)}
              />
            </label>
            <footer>
              <button type="button" onClick={() => setPhase("input")}>
                Back
              </button>
              <button
                type="button"
                className="primary"
                disabled={!externalResponse.trim()}
                onClick={acceptExternal}
              >
                Validate response
              </button>
            </footer>
          </div>
        )}
        {busy && (
          <div className="ai-workflow-progress">
            <div className="ai-workflow-phases">
              <span
                className={phase === "planning" ? "active" : plan ? "done" : ""}
              >
                1 Plan
              </span>
              <span
                className={phase === "generating"
                  ? "active"
                  : generatedBlock
                  ? "done"
                  : ""}
              >
                2 Generate
              </span>
              <span
                className={phase === "reviewing"
                  ? "active"
                  : review
                  ? "done"
                  : ""}
              >
                3 Review
              </span>
            </div>
            <RefreshCw className="spin" size={22} />
            <strong>
              {phase === "planning"
                ? "Planning…"
                : phase === "generating"
                ? "Generating and validating…"
                : "Reviewing…"}
            </strong>
            {thinking && (
              <details open>
                <summary>Thinking</summary>
                <pre>{thinking}</pre>
              </details>
            )}
            {streamText && (
              <details>
                <summary>Streaming output</summary>
                <pre>{streamText}</pre>
              </details>
            )}
          </div>
        )}
        {phase === "plan-review" && (
          <div className="ai-workflow-review">
            <h3>Plan</h3>
            <pre>{plan}</pre>
            <label>
              <span>Feedback for re-plan</span>
              <textarea
                rows={3}
                value={planFeedback}
                onChange={(event) => setPlanFeedback(event.target.value)}
              />
            </label>
            <footer>
              <button type="button" onClick={onClose}>Cancel</button>
              <button
                type="button"
                disabled={!planFeedback.trim()}
                onClick={() => void runPlanning(planFeedback)}
              >
                Re-plan
              </button>
              <button
                type="button"
                className="primary"
                onClick={() => void generate()}
              >
                <Check size={13} />Approve plan
              </button>
            </footer>
          </div>
        )}
        {phase === "result" && (
          <div className="ai-workflow-result">
            <div className="ai-workflow-phases">
              <span className="done">1 Plan</span>
              <span className="done">2 Generate</span>
              <span className="done">3 Review</span>
            </div>
            <section className={review?.verdict === "pass" ? "pass" : "fail"}>
              <strong>
                {review?.verdict === "pass"
                  ? "Review passed"
                  : "Review found issues"}
              </strong>
              <p>{review?.summary}</p>
              {review?.verdict === "pass" && review.issues.length > 0
                ? (
                  <details className="ai-workflow-improvements">
                    <summary>
                      {review.issues.length}{" "}
                      optional improvement{review.issues.length === 1
                        ? ""
                        : "s"}
                    </summary>
                    {review.issues.map((issue, index) => (
                      <article key={index}>
                        <b>
                          {issue.severity || "suggestion"}
                          {issue.nodeId ? ` · ${issue.nodeId}` : ""}
                        </b>
                        <span>{issue.message}</span>
                        {issue.suggestion && <small>{issue.suggestion}</small>}
                      </article>
                    ))}
                  </details>
                )
                : review?.issues.map((issue, index) => (
                  <article key={index}>
                    <b>
                      {issue.severity || "issue"}
                      {issue.nodeId ? ` · ${issue.nodeId}` : ""}
                    </b>
                    <span>{issue.message}</span>
                    {issue.suggestion && <small>{issue.suggestion}</small>}
                  </article>
                ))}
              {review?.verdict === "fail" && (
                <label className="ai-workflow-risk">
                  <input
                    type="checkbox"
                    checked={acceptReviewIssues}
                    onChange={(event) =>
                      setAcceptReviewIssues(event.target.checked)}
                  />I reviewed these issues and want to use the result without
                  another refinement.
                </label>
              )}
            </section>
            {artifactKind === "skill" && (
              <details open>
                <summary>Skill instructions</summary>
                <pre>{skillInstructions}</pre>
              </details>
            )}
            {mode === "modify"
              ? (
                <div className="ai-workflow-diff">
                  <UnifiedDiff
                    before={currentMarkdown}
                    after={`${
                      skillInstructions ? `${skillInstructions}\n\n` : ""
                    }${generatedBlock}`}
                  />
                </div>
              )
              : (
                <details open>
                  <summary>Generated workflow</summary>
                  <pre>{generatedBlock}</pre>
                </details>
              )}
            <label>
              <span>Additional refinement</span>
              <textarea
                rows={3}
                value={refineFeedback}
                onChange={(event) => setRefineFeedback(event.target.value)}
              />
            </label>
            <div className="ai-workflow-usage">
              {elapsedMs ? `${(elapsedMs / 1000).toFixed(1)}s` : ""}
              {usage
                ? ` · ${usage.inputTokens || 0} → ${
                  usage.outputTokens || 0
                } tokens${
                  usage.thinkingTokens
                    ? ` · Thinking ${usage.thinkingTokens}`
                    : ""
                }`
                : ""}
            </div>
            <footer>
              <button type="button" onClick={onClose}>Cancel</button>
              <button
                type="button"
                disabled={!refineFeedback.trim() && review?.verdict === "pass"}
                onClick={() =>
                  void generate(
                    `${review?.summary || ""}\n${
                      review?.issues.map((issue) =>
                        `${issue.severity}: ${issue.message} ${
                          issue.suggestion || ""
                        }`
                      ).join("\n")
                    }\n${refineFeedback}`,
                  )}
              >
                Refine
              </button>
              <button
                type="button"
                className="primary"
                disabled={review?.verdict === "fail" && !acceptReviewIssues}
                onClick={() => void apply()}
              >
                Use this {artifactKind}
              </button>
            </footer>
          </div>
        )}
        {error && <div className="workflow-error">{error}</div>}
      </section>
    </div>
  );
}
