"use client";

import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { PipelineNode } from "@/lib/pipeline-graph";

interface NodeFormProps {
  node: PipelineNode;
  onChange: (next: PipelineNode) => void;
}

// Typed forms per node type. Anything we don't know about renders a generic
// JSON view (handled by the inspector — this component returns null in that
// case so the parent shows the JSON fallback).
//
// Each form mutates node.parameters and calls onChange with the updated node.
// We deliberately keep these dumb (no internal state); the inspector owns
// debouncing and persistence.
export function NodeForm({ node, onChange }: NodeFormProps) {
  switch (node.type) {
    case "flow-nodes-base.trigger":
      return <TriggerForm node={node} onChange={onChange} />;
    case "flow-nodes-base.build":
      return <BuildForm node={node} onChange={onChange} />;
    case "flow-nodes-base.test":
      return <TestForm node={node} onChange={onChange} />;
    case "flow-nodes-base.eval":
      return <EvalForm node={node} onChange={onChange} />;
    case "flow-nodes-base.policy":
      return <PolicyForm node={node} onChange={onChange} />;
    case "flow-nodes-base.waitForApproval":
      return <ApprovalForm node={node} onChange={onChange} />;
    case "flow-nodes-base.sast":
      return <SastForm node={node} onChange={onChange} />;
    case "flow-nodes-base.imageScan":
      return <ImageScanForm node={node} onChange={onChange} />;
    case "flow-nodes-base.push":
      return <PushForm node={node} onChange={onChange} />;
    case "flow-nodes-base.deploy":
      return <DeployForm node={node} onChange={onChange} />;
    case "flow-nodes-base.promote":
      return <PromoteForm node={node} onChange={onChange} />;
    case "flow-nodes-base.rollback":
      return <RollbackForm node={node} onChange={onChange} />;
    default:
      return null;
  }
}

/** Returns true if we render a typed form for this type (so the JSON
 *  fallback can be hidden). */
export function hasTypedForm(type: string): boolean {
  return [
    "flow-nodes-base.trigger",
    "flow-nodes-base.build",
    "flow-nodes-base.sast",
    "flow-nodes-base.imageScan",
    "flow-nodes-base.push",
    "flow-nodes-base.test",
    "flow-nodes-base.eval",
    "flow-nodes-base.policy",
    "flow-nodes-base.waitForApproval",
    "flow-nodes-base.deploy",
    "flow-nodes-base.promote",
    "flow-nodes-base.rollback",
  ].includes(type);
}

// --- helpers --------------------------------------------------------------

function setParam<T>(node: PipelineNode, key: string, value: T): PipelineNode {
  return {
    ...node,
    parameters: { ...(node.parameters ?? {}), [key]: value },
  };
}

function getString(node: PipelineNode, key: string, fallback = ""): string {
  const v = node.parameters?.[key];
  return typeof v === "string" ? v : fallback;
}

function getNumber(node: PipelineNode, key: string, fallback = 0): number {
  const v = node.parameters?.[key];
  return typeof v === "number" ? v : fallback;
}

function getStringArray(node: PipelineNode, key: string): string[] {
  const v = node.parameters?.[key];
  return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
}

function getBool(node: PipelineNode, key: string, fallback: boolean): boolean {
  const v = node.parameters?.[key];
  if (typeof v === "boolean") return v;
  return fallback;
}

// --- forms ----------------------------------------------------------------

function TriggerForm({ node, onChange }: NodeFormProps) {
  const mode = getString(node, "mode", "manual");
  const cron = getString(node, "cron", "0 * * * *");
  const fromBranch = getString(node, "fromBranch", "main");
  const toBranch = getString(node, "toBranch", "production");
  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Mode</Label>
        <select
          value={mode}
          onChange={(e) => onChange(setParam(node, "mode", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="manual">Manual</option>
          <option value="webhook">Git webhook (push)</option>
          <option value="schedule">Scheduled (cron)</option>
        </select>
        <p className="text-[11px] text-muted-foreground">
          How runs are dispatched. Webhook + manual are wired today; schedule
          lands once the cron worker exists.
        </p>
      </div>
      {mode === "schedule" && (
        <div className="space-y-1.5">
          <Label htmlFor="cron">Cron expression</Label>
          <Input
            id="cron"
            value={cron}
            onChange={(e) => onChange(setParam(node, "cron", e.target.value))}
            placeholder="0 * * * *"
            className="font-mono text-xs"
          />
        </div>
      )}
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label htmlFor="trig-from">From branch</Label>
          <Input
            id="trig-from"
            value={fromBranch}
            onChange={(e) =>
              onChange(setParam(node, "fromBranch", e.target.value))
            }
            placeholder="main"
            className="font-mono text-xs"
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="trig-to">To branch</Label>
          <Input
            id="trig-to"
            value={toBranch}
            onChange={(e) =>
              onChange(setParam(node, "toBranch", e.target.value))
            }
            placeholder="production"
            className="font-mono text-xs"
          />
        </div>
      </div>
      <p className="text-[11px] text-muted-foreground">
        From/To branch travel in the trigger payload — Build clones{" "}
        <code>fromBranch</code>, Promote opens a PR{" "}
        <code>fromBranch → toBranch</code>.
      </p>
    </div>
  );
}

function BuildForm({ node, onChange }: NodeFormProps) {
  const mode = getString(node, "mode", "docker");
  const dockerfile = getString(node, "dockerfile", "Dockerfile");
  const ctx = getString(node, "context", ".");
  const imageName = getString(node, "imageName", "");
  const registry = getString(node, "registry", "registry:5000");
  const platform = getString(node, "platform", "linux/amd64");
  const buildArgs = getString(node, "buildArgs", "");

  const command = getString(
    node,
    "command",
    "docker build -t $AGENT_NAME:$COMMIT_SHA ."
  );
  const workdir = getString(node, "workdir", ".");
  const timeout = getNumber(node, "timeoutSeconds", 600);

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Mode</Label>
        <select
          value={mode}
          onChange={(e) => onChange(setParam(node, "mode", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="docker">docker</option>
          <option value="shell">shell</option>
        </select>
        <p className="text-[11px] text-muted-foreground">
          <code>docker</code> = real OCI image build &amp; push via BuildKit.{" "}
          <code>shell</code> = run any command (escape hatch).
        </p>
      </div>

      {mode === "docker" ? (
        <>
          <div className="space-y-1.5">
            <Label htmlFor="dockerfile">Dockerfile</Label>
            <Input
              id="dockerfile"
              value={dockerfile}
              onChange={(e) =>
                onChange(setParam(node, "dockerfile", e.target.value))
              }
              placeholder="Dockerfile"
              className="font-mono text-xs"
            />
            <p className="text-[11px] text-muted-foreground">
              Path relative to the repo root.
            </p>
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="b-ctx">Build context</Label>
            <Input
              id="b-ctx"
              value={ctx}
              onChange={(e) => onChange(setParam(node, "context", e.target.value))}
              placeholder="."
              className="font-mono text-xs"
            />
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="b-image">Image name (optional)</Label>
            <Input
              id="b-image"
              value={imageName}
              onChange={(e) =>
                onChange(setParam(node, "imageName", e.target.value))
              }
              placeholder="my-org/my-agent"
              className="font-mono text-xs"
            />
            <p className="text-[11px] text-muted-foreground">
              Defaults to <code>&lt;owner&gt;/&lt;repo&gt;</code> from the
              agent&rsquo;s connection.
            </p>
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="b-reg">Registry</Label>
            <Input
              id="b-reg"
              value={registry}
              onChange={(e) =>
                onChange(setParam(node, "registry", e.target.value))
              }
              placeholder="ghcr.io"
              className="font-mono text-xs"
            />
            <p className="text-[11px] text-muted-foreground">
              In docker-compose, <code>registry:5000</code> is the bundled
              local registry (host port 5050 for <code>docker pull</code>).
              For <code>ghcr.io</code>, the agent&rsquo;s PAT must have{" "}
              <code>write:packages</code>.
            </p>
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="b-plat">Target platform</Label>
            <Input
              id="b-plat"
              value={platform}
              onChange={(e) =>
                onChange(setParam(node, "platform", e.target.value))
              }
              placeholder="linux/amd64"
              className="font-mono text-xs"
            />
          </div>

          <div className="space-y-1.5">
            <Label htmlFor="b-args">Build args (optional)</Label>
            <Input
              id="b-args"
              value={buildArgs}
              onChange={(e) =>
                onChange(setParam(node, "buildArgs", e.target.value))
              }
              placeholder="NODE_ENV=production, FOO=bar"
              className="font-mono text-xs"
            />
            <p className="text-[11px] text-muted-foreground">
              <code>key=value</code>, comma-separated.
            </p>
          </div>
        </>
      ) : (
        <>
          <div className="space-y-1.5">
            <Label htmlFor="command">Command</Label>
            <Textarea
              id="command"
              rows={3}
              value={command}
              onChange={(e) => onChange(setParam(node, "command", e.target.value))}
              spellCheck={false}
              className="font-mono text-xs"
            />
            <p className="text-[11px] text-muted-foreground">
              Runs in a clone of the agent repo. Available env:{" "}
              <code>$AGENT_NAME</code>, <code>$REPO_URL</code>,{" "}
              <code>$COMMIT_SHA</code>, <code>$REF</code>.
            </p>
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="workdir">Workdir</Label>
            <Input
              id="workdir"
              value={workdir}
              onChange={(e) =>
                onChange(setParam(node, "workdir", e.target.value))
              }
              placeholder="."
              className="font-mono text-xs"
            />
          </div>
        </>
      )}

      <div className="space-y-1.5">
        <Label htmlFor="timeout">Timeout (seconds)</Label>
        <Input
          id="timeout"
          type="number"
          min={10}
          max={3600}
          value={timeout}
          onChange={(e) =>
            onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
          }
        />
      </div>
    </div>
  );
}

function TestForm({ node, onChange }: NodeFormProps) {
  const command = getString(node, "command", "pytest -q");
  const workdir = getString(node, "workdir", ".");
  const timeout = getNumber(node, "timeoutSeconds", 600);
  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label htmlFor="t-cmd">Command</Label>
        <Textarea
          id="t-cmd"
          rows={3}
          value={command}
          onChange={(e) => onChange(setParam(node, "command", e.target.value))}
          spellCheck={false}
          className="font-mono text-xs"
        />
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label htmlFor="t-wd">Workdir</Label>
          <Input
            id="t-wd"
            value={workdir}
            onChange={(e) => onChange(setParam(node, "workdir", e.target.value))}
            placeholder="."
            className="font-mono text-xs"
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="t-to">Timeout (s)</Label>
          <Input
            id="t-to"
            type="number"
            min={10}
            max={3600}
            value={timeout}
            onChange={(e) =>
              onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
            }
          />
        </div>
      </div>
      <p className="text-[11px] text-muted-foreground">
        Stub today: logs the command and returns success. Wire to a real
        executor when the test runner exists.
      </p>
    </div>
  );
}

function EvalForm({ node, onChange }: NodeFormProps) {
  const suite = getString(node, "suite", "default");
  const metric = getString(node, "metric", "accuracy");
  const threshold = getNumber(node, "threshold", 0.8);
  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label htmlFor="e-suite">Suite</Label>
        <Input
          id="e-suite"
          value={suite}
          onChange={(e) => onChange(setParam(node, "suite", e.target.value))}
          placeholder="default"
        />
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label htmlFor="e-metric">Metric</Label>
          <Input
            id="e-metric"
            value={metric}
            onChange={(e) => onChange(setParam(node, "metric", e.target.value))}
            placeholder="accuracy"
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="e-threshold">Threshold</Label>
          <Input
            id="e-threshold"
            type="number"
            step="0.01"
            min={0}
            max={1}
            value={threshold}
            onChange={(e) =>
              onChange(setParam(node, "threshold", Number(e.target.value)))
            }
          />
        </div>
      </div>
    </div>
  );
}

function PolicyForm({ node, onChange }: NodeFormProps) {
  const rules = getStringArray(node, "rules");
  const mode = getString(node, "mode", "enforce");
  const text = rules.join("\n");

  function commit(t: string) {
    const parsed = t.split("\n").map((s) => s.trim()).filter(Boolean);
    onChange(setParam(node, "rules", parsed));
  }

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Enforcement</Label>
        <select
          value={mode}
          onChange={(e) => onChange(setParam(node, "mode", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="enforce">Enforce — fail on violation</option>
          <option value="warn">Warn — log only</option>
          <option value="audit">Audit — record, never block</option>
        </select>
      </div>
      <div className="space-y-1.5">
        <Label htmlFor="p-rules">Rules (one per line)</Label>
        <Textarea
          id="p-rules"
          rows={6}
          defaultValue={text}
          onBlur={(e) => commit(e.target.value)}
          spellCheck={false}
          className="font-mono text-xs"
          placeholder={"max_monthly_spend_usd:1000\nno_pii_in_outputs"}
        />
      </div>
    </div>
  );
}

function ApprovalForm({ node, onChange }: NodeFormProps) {
  const reason = getString(node, "reason", "Manual review");
  const reviewers = getStringArray(node, "reviewers");
  const text = reviewers.join("\n");

  function commit(t: string) {
    const parsed = t.split("\n").map((s) => s.trim()).filter(Boolean);
    onChange(setParam(node, "reviewers", parsed));
  }

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label htmlFor="a-reason">Reason</Label>
        <Input
          id="a-reason"
          value={reason}
          onChange={(e) => onChange(setParam(node, "reason", e.target.value))}
          placeholder="Manual review before deploy"
        />
        <p className="text-[11px] text-muted-foreground">
          Shown in the Resume panel on the executions page.
        </p>
      </div>
      <div className="space-y-1.5">
        <Label htmlFor="a-reviewers">Reviewers (one per line)</Label>
        <Textarea
          id="a-reviewers"
          rows={4}
          defaultValue={text}
          onBlur={(e) => commit(e.target.value)}
          spellCheck={false}
          className="font-mono text-xs"
          placeholder="user@example.com"
        />
      </div>
    </div>
  );
}

function SastForm({ node, onChange }: NodeFormProps) {
  const tool = getString(node, "tool", "trivy");
  const threshold = getString(node, "severityThreshold", "HIGH");
  const failOnFinding = getBool(node, "failOnFinding", true);
  const timeout = getNumber(node, "timeoutSeconds", 600);

  // sonar
  const sonarHost = getString(node, "sonarHost", "https://sonarcloud.io");
  const organization = getString(node, "organization", "");
  const projectKey = getString(node, "projectKey", "");
  const sonarToken = getString(node, "sonarToken", "");
  const branchName = getString(node, "branchName", "");

  // custom
  const image = getString(node, "image", "");
  const command = getString(node, "command", "");

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Tool</Label>
        <select
          value={tool}
          onChange={(e) => onChange(setParam(node, "tool", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="trivy">Trivy — vulns + secrets + IaC misconfig</option>
          <option value="semgrep">Semgrep — code-flow / taint analysis</option>
          <option value="gitleaks">Gitleaks — leaked secrets</option>
          <option value="sonar">SonarCloud — code quality + quality gate</option>
          <option value="custom">Custom — your container, your command</option>
        </select>
      </div>

      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label>Fail-at severity</Label>
          <select
            value={threshold}
            onChange={(e) =>
              onChange(setParam(node, "severityThreshold", e.target.value))
            }
            className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
          >
            <option value="LOW">LOW (everything)</option>
            <option value="MEDIUM">MEDIUM</option>
            <option value="HIGH">HIGH (default)</option>
            <option value="CRITICAL">CRITICAL only</option>
          </select>
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="sast-timeout">Timeout (s)</Label>
          <Input
            id="sast-timeout"
            type="number"
            min={30}
            max={3600}
            value={timeout}
            onChange={(e) =>
              onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
            }
          />
        </div>
      </div>

      <div className="flex items-center gap-2">
        <input
          id="sast-fail"
          type="checkbox"
          checked={failOnFinding}
          onChange={(e) =>
            onChange(setParam(node, "failOnFinding", e.target.checked))
          }
          className="size-3.5"
        />
        <Label htmlFor="sast-fail" className="text-[11px]">
          Fail the run when findings exceed threshold (default on)
        </Label>
      </div>

      {tool === "sonar" && (
        <div className="rounded-md border bg-muted/20 p-3 space-y-2">
          <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
            SonarCloud
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="sonar-host">Host</Label>
            <Input
              id="sonar-host"
              value={sonarHost}
              onChange={(e) =>
                onChange(setParam(node, "sonarHost", e.target.value))
              }
              placeholder="https://sonarcloud.io"
              className="font-mono text-xs"
            />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <div className="space-y-1.5">
              <Label htmlFor="sonar-org">Organization</Label>
              <Input
                id="sonar-org"
                value={organization}
                onChange={(e) =>
                  onChange(setParam(node, "organization", e.target.value))
                }
                placeholder="my-org"
                className="font-mono text-xs"
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="sonar-key">Project key</Label>
              <Input
                id="sonar-key"
                value={projectKey}
                onChange={(e) =>
                  onChange(setParam(node, "projectKey", e.target.value))
                }
                placeholder="my-org_my-agent"
                className="font-mono text-xs"
              />
            </div>
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="sonar-token">Token</Label>
            <Input
              id="sonar-token"
              type="password"
              value={sonarToken}
              onChange={(e) =>
                onChange(setParam(node, "sonarToken", e.target.value))
              }
              placeholder="SONAR_TOKEN (User → My Account → Security)"
              autoComplete="new-password"
              className="font-mono text-xs"
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="sonar-branch">Branch (optional)</Label>
            <Input
              id="sonar-branch"
              value={branchName}
              onChange={(e) =>
                onChange(setParam(node, "branchName", e.target.value))
              }
              placeholder="(uses agent ref by default)"
              className="font-mono text-xs"
            />
          </div>
          <p className="text-[11px] text-muted-foreground">
            Quality-gate failure flips the run to <code>failed</code>. We
            also link the dashboard URL on the run page.
          </p>
        </div>
      )}

      {tool === "custom" && (
        <div className="rounded-md border bg-muted/20 p-3 space-y-2">
          <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
            Custom scanner
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="cust-img">Container image</Label>
            <Input
              id="cust-img"
              value={image}
              onChange={(e) =>
                onChange(setParam(node, "image", e.target.value))
              }
              placeholder="ghcr.io/owner/scanner:latest"
              className="font-mono text-xs"
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="cust-cmd">Command (runs in /src)</Label>
            <Textarea
              id="cust-cmd"
              rows={3}
              value={command}
              onChange={(e) =>
                onChange(setParam(node, "command", e.target.value))
              }
              spellCheck={false}
              placeholder="my-scanner --src /src --json"
              className="font-mono text-xs"
            />
          </div>
          <p className="text-[11px] text-muted-foreground">
            Repo is mounted at <code>/src</code> read-only. Non-zero exit
            fails the node.
          </p>
        </div>
      )}

      {tool !== "sonar" && tool !== "custom" && (
        <p className="text-[11px] text-muted-foreground">
          Sane defaults — no extra config needed. Findings list ends up in{" "}
          <code className="font-mono">__sast.findings</code> and per-finding
          lines stream into the build log.
        </p>
      )}
    </div>
  );
}

function ImageScanForm({ node, onChange }: NodeFormProps) {
  const tool = getString(node, "tool", "trivy");
  const threshold = getString(node, "severityThreshold", "HIGH");
  const failOnFinding = getBool(node, "failOnFinding", true);
  const timeout = getNumber(node, "timeoutSeconds", 600);
  const insecure = getBool(node, "insecure", true);
  const imageRef = getString(node, "imageRef", "");
  const registryUsername = getString(node, "registryUsername", "");
  const registryPassword = getString(node, "registryPassword", "");
  const image = getString(node, "image", "");
  const command = getString(node, "command", "");

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Tool</Label>
        <select
          value={tool}
          onChange={(e) => onChange(setParam(node, "tool", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="trivy">Trivy — CVEs + secrets in the image</option>
          <option value="grype">Grype — Anchore CVE scanner</option>
          <option value="custom">Custom — your container, your command</option>
        </select>
      </div>

      <div className="space-y-1.5">
        <Label htmlFor="is-ref">Image ref (optional)</Label>
        <Input
          id="is-ref"
          value={imageRef}
          onChange={(e) => onChange(setParam(node, "imageRef", e.target.value))}
          placeholder="registry:5000/owner/agent:sha (defaults to upstream Build)"
          className="font-mono text-xs"
        />
        <p className="text-[11px] text-muted-foreground">
          Leave blank to use upstream{" "}
          <code className="font-mono">__build.image</code>.
        </p>
      </div>

      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label>Fail-at severity</Label>
          <select
            value={threshold}
            onChange={(e) =>
              onChange(setParam(node, "severityThreshold", e.target.value))
            }
            className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
          >
            <option value="LOW">LOW</option>
            <option value="MEDIUM">MEDIUM</option>
            <option value="HIGH">HIGH (default)</option>
            <option value="CRITICAL">CRITICAL only</option>
          </select>
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="is-timeout">Timeout (s)</Label>
          <Input
            id="is-timeout"
            type="number"
            min={30}
            max={3600}
            value={timeout}
            onChange={(e) =>
              onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
            }
          />
        </div>
      </div>

      <div className="flex items-center gap-2">
        <input
          id="is-fail"
          type="checkbox"
          checked={failOnFinding}
          onChange={(e) =>
            onChange(setParam(node, "failOnFinding", e.target.checked))
          }
          className="size-3.5"
        />
        <Label htmlFor="is-fail" className="text-[11px]">
          Fail run when findings exceed threshold
        </Label>
      </div>
      <div className="flex items-center gap-2">
        <input
          id="is-insecure"
          type="checkbox"
          checked={insecure}
          onChange={(e) =>
            onChange(setParam(node, "insecure", e.target.checked))
          }
          className="size-3.5"
        />
        <Label htmlFor="is-insecure" className="text-[11px]">
          Source registry allows HTTP (default; the local{" "}
          <code className="font-mono">registry:5000</code> is plain HTTP)
        </Label>
      </div>

      {tool !== "custom" && (
        <div className="rounded-md border bg-muted/20 p-3 space-y-2">
          <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
            Registry auth (optional — only needed for private sources)
          </div>
          <div className="grid grid-cols-2 gap-2">
            <div className="space-y-1.5">
              <Label htmlFor="is-user">Username</Label>
              <Input
                id="is-user"
                value={registryUsername}
                onChange={(e) =>
                  onChange(setParam(node, "registryUsername", e.target.value))
                }
                placeholder="(empty = anonymous)"
                autoComplete="off"
                className="font-mono text-xs"
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="is-pass">Password / token</Label>
              <Input
                id="is-pass"
                type="password"
                value={registryPassword}
                onChange={(e) =>
                  onChange(setParam(node, "registryPassword", e.target.value))
                }
                placeholder="ghp_… or registry password"
                autoComplete="new-password"
                className="font-mono text-xs"
              />
            </div>
          </div>
        </div>
      )}

      {tool === "custom" && (
        <div className="rounded-md border bg-muted/20 p-3 space-y-2">
          <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
            Custom scanner
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="is-cust-img">Container image</Label>
            <Input
              id="is-cust-img"
              value={image}
              onChange={(e) => onChange(setParam(node, "image", e.target.value))}
              placeholder="ghcr.io/owner/scanner:latest"
              className="font-mono text-xs"
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="is-cust-cmd">
              Command (image ref exported as <code>$IMAGE_REF</code>)
            </Label>
            <Textarea
              id="is-cust-cmd"
              rows={3}
              value={command}
              onChange={(e) =>
                onChange(setParam(node, "command", e.target.value))
              }
              spellCheck={false}
              placeholder='trivy image --quiet "$IMAGE_REF"'
              className="font-mono text-xs"
            />
          </div>
        </div>
      )}
    </div>
  );
}

type PushTarget = {
  name?: string;
  registry?: string;
  image?: string;
  tag?: string;
  username?: string;
  password?: string;
  insecure?: boolean;
};

function PushForm({ node, onChange }: NodeFormProps) {
  const srcImage = getString(node, "srcImage", "");
  const srcInsecure = getBool(node, "srcInsecure", true);
  const rawTargets = (node.parameters?.targets as unknown) as PushTarget[] | undefined;
  const targets: PushTarget[] = Array.isArray(rawTargets) ? rawTargets : [];

  function setTargets(next: PushTarget[]) {
    onChange(setParam(node, "targets", next as unknown as Record<string, unknown>[]));
  }
  function patch(idx: number, patch: Partial<PushTarget>) {
    setTargets(targets.map((t, i) => (i === idx ? { ...t, ...patch } : t)));
  }
  function addTarget() {
    setTargets([
      ...targets,
      { name: `target-${targets.length + 1}`, registry: "", image: "" },
    ]);
  }
  function removeTarget(idx: number) {
    setTargets(targets.filter((_, i) => i !== idx));
  }

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label htmlFor="push-src">Source image (optional)</Label>
        <Input
          id="push-src"
          value={srcImage}
          onChange={(e) => onChange(setParam(node, "srcImage", e.target.value))}
          placeholder="registry:5000/owner/agent:sha (defaults to upstream Build)"
          className="font-mono text-xs"
        />
        <p className="text-[11px] text-muted-foreground">
          Leave blank to use the upstream Build node&rsquo;s{" "}
          <code className="font-mono">__build.image</code>.
        </p>
      </div>

      <div className="flex items-center gap-2">
        <input
          id="push-src-insecure"
          type="checkbox"
          checked={srcInsecure}
          onChange={(e) => onChange(setParam(node, "srcInsecure", e.target.checked))}
          className="size-3.5"
        />
        <Label htmlFor="push-src-insecure" className="text-[11px]">
          Source allows HTTP (default — the local{" "}
          <code className="font-mono">registry:5000</code> is plain HTTP)
        </Label>
      </div>

      <div className="space-y-2">
        <div className="flex items-center justify-between">
          <Label className="text-xs uppercase tracking-wider">
            Targets ({targets.length})
          </Label>
          <button
            type="button"
            onClick={addTarget}
            className="rounded-md border bg-background px-2 py-1 text-[11px] hover:bg-accent"
          >
            + add target
          </button>
        </div>

        {targets.length === 0 && (
          <p className="rounded-md border border-amber-500/40 bg-amber-500/5 p-2 text-[11px] text-amber-700 dark:text-amber-400">
            No targets configured — Push will fail at run time. Click “+ add
            target”.
          </p>
        )}

        {targets.map((t, idx) => (
          <TargetRow
            key={idx}
            idx={idx}
            target={t}
            onPatch={(p) => patch(idx, p)}
            onRemove={() => removeTarget(idx)}
          />
        ))}
      </div>

      <p className="text-[11px] text-muted-foreground">
        Each target runs in parallel. Image is pulled from the source once and
        pushed concurrently — no docker daemon needed. For GHCR, password is a
        PAT with <code className="font-mono">write:packages</code>.
      </p>
    </div>
  );
}

function TargetRow({
  idx,
  target,
  onPatch,
  onRemove,
}: {
  idx: number;
  target: PushTarget;
  onPatch: (p: Partial<PushTarget>) => void;
  onRemove: () => void;
}) {
  const ref = `${target.registry || "<registry>"}/${target.image || "<image>"}:${target.tag || "<tag>"}`;
  return (
    <div className="rounded-md border bg-muted/10 p-2 space-y-2">
      <div className="flex items-center gap-2">
        <Input
          value={target.name ?? ""}
          onChange={(e) => onPatch({ name: e.target.value })}
          placeholder={`target-${idx + 1}`}
          className="h-7 max-w-[140px] font-mono text-[11px]"
        />
        <span className="flex-1 truncate font-mono text-[10px] text-muted-foreground">
          {ref}
        </span>
        <button
          type="button"
          onClick={onRemove}
          className="rounded-md border bg-background px-2 py-1 text-[11px] text-destructive hover:bg-destructive/10"
          aria-label="Remove target"
        >
          remove
        </button>
      </div>

      <div className="grid grid-cols-2 gap-2">
        <div className="space-y-1">
          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">
            Registry
          </Label>
          <Input
            value={target.registry ?? ""}
            onChange={(e) => onPatch({ registry: e.target.value })}
            placeholder="ghcr.io"
            className="h-7 font-mono text-[11px]"
          />
        </div>
        <div className="space-y-1">
          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">
            Tag
          </Label>
          <Input
            value={target.tag ?? ""}
            onChange={(e) => onPatch({ tag: e.target.value })}
            placeholder="(commit sha → latest)"
            className="h-7 font-mono text-[11px]"
          />
        </div>
      </div>

      <div className="space-y-1">
        <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">
          Image
        </Label>
        <Input
          value={target.image ?? ""}
          onChange={(e) => onPatch({ image: e.target.value })}
          placeholder="org/agent"
          className="h-7 font-mono text-[11px]"
        />
      </div>

      <div className="grid grid-cols-2 gap-2">
        <div className="space-y-1">
          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">
            Username
          </Label>
          <Input
            value={target.username ?? ""}
            onChange={(e) => onPatch({ username: e.target.value })}
            placeholder="(anonymous)"
            autoComplete="off"
            className="h-7 font-mono text-[11px]"
          />
        </div>
        <div className="space-y-1">
          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">
            Password / token
          </Label>
          <Input
            type="password"
            value={target.password ?? ""}
            onChange={(e) => onPatch({ password: e.target.value })}
            placeholder="ghp_…"
            autoComplete="new-password"
            className="h-7 font-mono text-[11px]"
          />
        </div>
      </div>

      <div className="flex items-center gap-2">
        <input
          id={`push-target-${idx}-insecure`}
          type="checkbox"
          checked={Boolean(target.insecure)}
          onChange={(e) => onPatch({ insecure: e.target.checked })}
          className="size-3.5"
        />
        <Label
          htmlFor={`push-target-${idx}-insecure`}
          className="text-[10px] text-muted-foreground"
        >
          Allow HTTP (only for local / private registries)
        </Label>
      </div>
    </div>
  );
}

function DeployForm({ node, onChange }: NodeFormProps) {
  const target = getString(node, "target", "agentcore");
  const credentialName = getString(node, "credentialName", "aws");
  const runtimeName = getString(node, "runtimeName", "");
  const image = getString(node, "image", "");
  const timeout = getNumber(node, "timeoutSeconds", 600);

  const rawEnv = (node.parameters?.envVars ?? {}) as Record<string, unknown>;
  const envEntries: [string, string][] = Object.entries(rawEnv).map(
    ([k, v]) => [k, typeof v === "string" ? v : String(v ?? "")]
  );

  function setEnvFromEntries(entries: [string, string][]) {
    const obj: Record<string, string> = {};
    for (const [k, v] of entries) {
      const key = k.trim();
      if (key) obj[key] = v;
    }
    onChange(setParam(node, "envVars", obj));
  }

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Target</Label>
        <select
          value={target}
          onChange={(e) => onChange(setParam(node, "target", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="agentcore">AWS Bedrock AgentCore</option>
          <option value="kubernetes" disabled>
            Kubernetes (coming soon)
          </option>
          <option value="vertex" disabled>
            GCP Vertex Agent Engine (coming soon)
          </option>
        </select>
        <p className="text-[11px] text-muted-foreground">
          AgentCore deploys the upstream Push image. AWS region + account +
          cross-account role come from the named credential below. The deploy
          summary will include the public invoke URL.
        </p>
      </div>

      <div className="space-y-1.5">
        <Label htmlFor="d-cred">Credential name</Label>
        <Input
          id="d-cred"
          value={credentialName}
          onChange={(e) =>
            onChange(setParam(node, "credentialName", e.target.value))
          }
          placeholder="aws"
          className="font-mono text-xs"
        />
        <p className="text-[11px] text-muted-foreground">
          Must match a credential of type <code>aws</code> on the Agent
          (Credentials section on the agent page).
        </p>
      </div>

      <div className="space-y-1.5">
        <Label htmlFor="d-runtime-name">Runtime name (override)</Label>
        <Input
          id="d-runtime-name"
          value={runtimeName}
          onChange={(e) =>
            onChange(setParam(node, "runtimeName", e.target.value))
          }
          placeholder="(defaults to agent name; AgentCore enforces [a-zA-Z0-9_]{1,48})"
          className="font-mono text-xs"
        />
      </div>

      <div className="space-y-1.5">
        <Label htmlFor="d-image">Image (override)</Label>
        <Input
          id="d-image"
          value={image}
          onChange={(e) => onChange(setParam(node, "image", e.target.value))}
          placeholder="(defaults to upstream Push __push.copies[0].imageRef)"
          className="font-mono text-xs"
        />
        <p className="text-[11px] text-muted-foreground">
          Resolution order: this field → Push output → Build output.
        </p>
      </div>

      <div className="space-y-1.5">
        <Label>Runtime env vars</Label>
        {envEntries.length === 0 && (
          <p className="text-[11px] text-muted-foreground">
            No env vars set. AgentCore receives these at runtime (not baked
            into the image).
          </p>
        )}
        {envEntries.map(([k, v], i) => (
          <div key={i} className="flex gap-2">
            <Input
              value={k}
              onChange={(e) => {
                const next = envEntries.slice();
                next[i] = [e.target.value, v];
                setEnvFromEntries(next);
              }}
              placeholder="KEY"
              className="font-mono text-xs"
            />
            <Input
              value={v}
              onChange={(e) => {
                const next = envEntries.slice();
                next[i] = [k, e.target.value];
                setEnvFromEntries(next);
              }}
              placeholder="value"
              className="font-mono text-xs"
            />
            <button
              type="button"
              onClick={() => {
                const next = envEntries.filter((_, idx) => idx !== i);
                setEnvFromEntries(next);
              }}
              className="rounded-md border border-input px-2 text-xs hover:bg-muted"
            >
              ✕
            </button>
          </div>
        ))}
        <button
          type="button"
          onClick={() => setEnvFromEntries([...envEntries, ["", ""]])}
          className="rounded-md border border-input px-2 py-1 text-xs hover:bg-muted"
        >
          + Add env var
        </button>
      </div>

      <div className="space-y-1.5">
        <Label htmlFor="d-timeout">Timeout (seconds)</Label>
        <Input
          id="d-timeout"
          type="number"
          value={timeout}
          onChange={(e) =>
            onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
          }
          className="font-mono text-xs"
        />
        <p className="text-[11px] text-muted-foreground">
          Caps the create + endpoint-readiness wait. AgentCore endpoints
          typically reach READY in 60–180s.
        </p>
      </div>
    </div>
  );
}

function PromoteForm({ node, onChange }: NodeFormProps) {
  const mode = getString(node, "mode", "open-pr");
  const fromBranch = getString(node, "fromBranch", "");
  const toBranch = getString(node, "toBranch", "");
  const title = getString(node, "title", "");
  const body = getString(node, "body", "Promotion opened by Flow.");
  const mergeMethod = getString(node, "mergeMethod", "merge");
  const timeout = getNumber(node, "timeoutSeconds", 60);

  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label>Mode</Label>
        <select
          value={mode}
          onChange={(e) => onChange(setParam(node, "mode", e.target.value))}
          className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
        >
          <option value="open-pr">Open pull request</option>
          <option value="merge">Merge branches directly</option>
          <option value="merge-pr">Open + merge pull request</option>
        </select>
        <p className="text-[11px] text-muted-foreground">
          Promote uses the agent&rsquo;s PAT to talk to GitHub.{" "}
          <strong>open-pr</strong> opens a PR (idempotent — re-finds an existing
          one); <strong>merge</strong> POSTs to <code>/merges</code>;{" "}
          <strong>merge-pr</strong> opens then merges.
        </p>
      </div>

      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1.5">
          <Label htmlFor="pr-from">From branch (override)</Label>
          <Input
            id="pr-from"
            value={fromBranch}
            onChange={(e) =>
              onChange(setParam(node, "fromBranch", e.target.value))
            }
            placeholder="(use Trigger fromBranch)"
            className="font-mono text-xs"
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="pr-to">To branch (override)</Label>
          <Input
            id="pr-to"
            value={toBranch}
            onChange={(e) =>
              onChange(setParam(node, "toBranch", e.target.value))
            }
            placeholder="(use Trigger toBranch)"
            className="font-mono text-xs"
          />
        </div>
      </div>

      {(mode === "open-pr" || mode === "merge-pr") && (
        <>
          <div className="space-y-1.5">
            <Label htmlFor="pr-title">PR title</Label>
            <Input
              id="pr-title"
              value={title}
              onChange={(e) =>
                onChange(setParam(node, "title", e.target.value))
              }
              placeholder="Promote {{fromBranch}} → {{toBranch}}"
            />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="pr-body">PR body</Label>
            <textarea
              id="pr-body"
              value={body}
              onChange={(e) =>
                onChange(setParam(node, "body", e.target.value))
              }
              rows={3}
              className="w-full rounded-md border border-input bg-transparent p-2 text-xs"
            />
          </div>
        </>
      )}

      {mode === "merge-pr" && (
        <div className="space-y-1.5">
          <Label>Merge method</Label>
          <select
            value={mergeMethod}
            onChange={(e) =>
              onChange(setParam(node, "mergeMethod", e.target.value))
            }
            className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
          >
            <option value="merge">Merge commit</option>
            <option value="squash">Squash</option>
            <option value="rebase">Rebase</option>
          </select>
        </div>
      )}

      <div className="space-y-1.5">
        <Label htmlFor="pr-to-secs">Timeout (seconds)</Label>
        <Input
          id="pr-to-secs"
          type="number"
          value={timeout}
          onChange={(e) =>
            onChange(setParam(node, "timeoutSeconds", Number(e.target.value)))
          }
          className="font-mono text-xs"
        />
      </div>

      <p className="text-[11px] text-muted-foreground">
        Branches default to the Trigger node&rsquo;s{" "}
        <code>fromBranch</code> / <code>toBranch</code> when these fields are
        empty.
      </p>
    </div>
  );
}

function RollbackForm({ node, onChange }: NodeFormProps) {
  const revision = getString(node, "revision", "previous");
  return (
    <div className="space-y-3">
      <div className="space-y-1.5">
        <Label htmlFor="rb-rev">Revision</Label>
        <Input
          id="rb-rev"
          value={revision}
          onChange={(e) => onChange(setParam(node, "revision", e.target.value))}
          placeholder='"previous" or a specific build ID'
          className="font-mono text-xs"
        />
      </div>
    </div>
  );
}
