'use client';

/**
 * ScheduleCreateModal — 3-step wizard:
 *   1. Pick body (kind: pipeline | prompt, then ref)
 *   2. Fill input (schema-driven for pipeline; project + agent + skills for prompt)
 *   3. Trigger (period / cron / once / manual + name) + action (none / chat / email / telegram)
 */

import { useEffect, useState } from 'react';

type BodyKind = 'pipeline' | 'prompt';

interface AgentInfo { id: string; label?: string; backendType?: string; }

interface Workflow { name: string; description?: string; }
interface SkillItem { name: string; displayName?: string; description?: string; installedGlobal?: boolean; installedProjects?: string[]; }
interface ProjectInfo { path: string; name: string; }
interface PipelineInputField {
  name: string;
  description: string;
  label?: string;
  required: boolean;
  type: 'string' | 'integer' | 'number' | 'boolean' | 'enum';
  enum?: string[] | null;
  default?: string | number | boolean | null;
  multiline?: boolean;
}
interface PipelineSchema { name: string; description: string | null; input: PipelineInputField[]; }

/** Subset of Schedule fields needed to seed the modal in edit mode. */
export interface EditableSchedule {
  id: string;
  name: string;
  body_kind: BodyKind;
  body_ref: string;
  input: Record<string, unknown>;
  skills?: string[];
  action_kind: 'none' | 'chat' | 'email' | 'telegram';
  action_config: Record<string, unknown>;
  schedule_kind: 'period' | 'once' | 'cron' | 'manual';
  schedule_interval_minutes: number;
  schedule_at: string | null;
  schedule_cron: string | null;
}

interface Props {
  onClose: () => void;
  onCreated: () => void;
  /** When set, the modal opens in edit mode: body_kind/body_ref locked,
   *  submit issues PATCH instead of POST, title/button labels swap. */
  existing?: EditableSchedule | null;
}

// Cast Schedule.input (Record<string, unknown>) into the modal's string-keyed
// form. The form coerces everything back to strings anyway; numbers/booleans
// stringify cleanly.
function inputToStrings(v: Record<string, unknown> | undefined): Record<string, string> {
  if (!v) return {};
  const out: Record<string, string> = {};
  for (const [k, val] of Object.entries(v)) out[k] = val == null ? '' : String(val);
  return out;
}

// Convert an ISO-UTC string ("2026-05-23T12:00:00.000Z") into the YYYY-MM-DDTHH:MM
// local form <input type="datetime-local"> requires. Strips seconds + tz.
function toLocalDatetimeInput(iso: string): string {
  const d = new Date(iso);
  if (isNaN(d.getTime())) return '';
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

export default function ScheduleCreateModal({ onClose, onCreated, existing }: Props) {
  const editing = !!existing;

  // In edit mode skip past Step 1 (body_kind / body_ref are immutable).
  const [step, setStep] = useState<1 | 2 | 3>(editing ? 2 : 1);

  // Step 1
  const [bodyKind, setBodyKind] = useState<BodyKind>(existing?.body_kind ?? 'pipeline');
  const [bodyRef, setBodyRef] = useState<string>(existing?.body_ref ?? '');
  const [workflows, setWorkflows] = useState<Workflow[]>([]);
  const [projects, setProjects] = useState<ProjectInfo[]>([]);

  // Step 2 — for pipeline body
  const [pipelineSchema, setPipelineSchema] = useState<PipelineSchema | null>(null);

  // Step 2 — for prompt body (V3). prompt text + agent + skills are
  // persisted in prompts/<body_ref>.yaml; the schedule row only carries
  // body_ref + input.project. In edit mode we GET the yaml to seed these.
  const [promptProject, setPromptProject] = useState(() => {
    if (existing?.body_kind !== 'prompt') return '';
    const p = String(existing.input?.project ?? '');
    // Backend stores 'scratch' for prompts that opted into the default. Map
    // it back to '' so the "— scratch (default) —" placeholder option matches.
    return p === 'scratch' ? '' : p;
  });
  const [promptText, setPromptText] = useState('');
  const [promptAgent, setPromptAgent] = useState<string>('');
  const [promptSkills, setPromptSkills] = useState<string[]>(['ai-orchestration']);
  const [agents, setAgents] = useState<AgentInfo[]>([]);
  const [promptLoadedRef, setPromptLoadedRef] = useState<string>('');

  const [input, setInput] = useState<Record<string, string>>(
    existing?.body_kind === 'pipeline' ? inputToStrings(existing.input) : {}
  );

  // Extra skills attached to the dispatched body (pipeline tasks).
  // Parallel to Job.skills — forwarded as --append-system-prompt; auto-installed
  // into the target project on dispatch.
  const [extraSkills, setExtraSkills] = useState<string[]>(Array.isArray(existing?.skills) ? existing!.skills! : []);
  const [extraSkillsPickerOpen, setExtraSkillsPickerOpen] = useState(false);
  // Auto-install handles missing ones — list ALL skills.
  const [allSkills, setAllSkills] = useState<SkillItem[]>([]);

  // Step 3
  const [name, setName] = useState(existing?.name ?? '');
  const [trigger, setTrigger] = useState<'period' | 'cron' | 'once' | 'manual'>(existing?.schedule_kind ?? 'period');
  const [interval, setInterval] = useState(existing?.schedule_interval_minutes ?? 10);
  const [cron, setCron] = useState(existing?.schedule_cron ?? '*/15 * * * *');
  const [scheduleAt, setScheduleAt] = useState(
    // <input type=datetime-local> expects YYYY-MM-DDTHH:MM (local), not ISO Z.
    existing?.schedule_at ? toLocalDatetimeInput(existing.schedule_at) : ''
  );
  const [actionKind, setActionKind] = useState<'none' | 'chat' | 'email' | 'telegram'>(existing?.action_kind ?? 'none');
  const [chatSessionId, setChatSessionId] = useState(
    existing?.action_kind === 'chat' ? String(existing.action_config?.session_id ?? '') : ''
  );
  const [chatPrefix, setChatPrefix] = useState(
    existing?.action_kind === 'chat' ? String(existing.action_config?.prefix ?? '') : ''
  );
  const [chatSessions, setChatSessions] = useState<Array<{ id: string; title: string }>>([]);
  const [emailTo, setEmailTo] = useState(
    existing?.action_kind === 'email'
      ? (Array.isArray(existing.action_config?.to)
          ? (existing.action_config.to as string[]).join(', ')
          : String(existing.action_config?.to ?? ''))
      : ''
  );
  const [emailSubjectTpl, setEmailSubjectTpl] = useState(
    existing?.action_kind === 'email'
      ? String(existing.action_config?.subject_template ?? 'Forge schedule — {date}')
      : 'Forge schedule — {date}'
  );
  const [emailBodyTpl, setEmailBodyTpl] = useState(
    existing?.action_kind === 'email'
      ? String(existing.action_config?.body_template ?? '{body_output}')
      : '{body_output}'
  );
  const [telegramChatId, setTelegramChatId] = useState(
    existing?.action_kind === 'telegram' ? String(existing.action_config?.chat_id ?? '') : ''
  );
  const [telegramPrefix, setTelegramPrefix] = useState(
    existing?.action_kind === 'telegram' ? String(existing.action_config?.prefix ?? '') : ''
  );

  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState('');

  // Load source lists on mount
  useEffect(() => {
    (async () => {
      try {
        const [wfR, skR, csR, agR] = await Promise.all([
          fetch('/api/pipelines?type=workflows').then((r) => r.json()),
          fetch('/api/skills').then((r) => r.json()),
          fetch('/api/chat-proxy/sessions').then((r) => r.json()).catch(() => ({ sessions: [] })),
          fetch('/api/agents?include=all').then((r) => r.json()).catch(() => ({ agents: [] })),
        ]);
        if (Array.isArray(agR?.agents)) setAgents(agR.agents);
        if (Array.isArray(csR?.sessions)) setChatSessions(csR.sessions);
        if (Array.isArray(wfR)) setWorkflows(wfR);
        else if (wfR?.workflows) setWorkflows(wfR.workflows);
        if (Array.isArray(skR?.skills)) {
          // Extra-skills picker lists everything — Forge auto-installs missing
          // ones into the target project at dispatch time.
          setAllSkills(skR.skills);
        }
        if (skR?.projects) setProjects(skR.projects);
      } catch (e) {
        setErr(e instanceof Error ? e.message : String(e));
      }
    })();
  }, []);

  // Load pipeline schema when pipeline body picked
  useEffect(() => {
    if (bodyKind !== 'pipeline' || !bodyRef) { setPipelineSchema(null); return; }
    (async () => {
      try {
        const r = await fetch(`/api/pipelines/${encodeURIComponent(bodyRef)}/schema`);
        if (!r.ok) {
          const j = await r.json().catch(() => ({}));
          setErr(j.error || `Schema fetch failed (${r.status})`);
          return;
        }
        const j = (await r.json()) as PipelineSchema;
        setPipelineSchema(j);
        const initial: Record<string, string> = {};
        for (const f of j.input) {
          // Prefer existing user-typed value, then the schema-declared
          // default (so v2 multiline defaults like user_prompt / mr_body_template
          // pre-fill the textarea instead of being empty placeholder hints).
          initial[f.name] = input[f.name] ?? (f.default != null ? String(f.default) : '');
        }
        setInput(initial);
        if (!name) {
          const firstVal = Object.values(initial).find((v) => v.length > 0);
          setName(`${bodyRef}${firstVal ? ' · ' + firstVal : ''}`);
        }
      } catch (e) {
        setErr(e instanceof Error ? e.message : String(e));
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [bodyKind, bodyRef]);

  // Edit mode: load existing prompt yaml so Step 2 form is pre-filled.
  // For new schedules nothing to load; user types the prompt fresh.
  useEffect(() => {
    if (!editing) return;
    if (existing?.body_kind !== 'prompt') return;
    if (promptLoadedRef === existing.body_ref) return;
    (async () => {
      try {
        const r = await fetch(`/api/prompts/${encodeURIComponent(existing.body_ref)}`);
        if (!r.ok) { setErr(`failed to load prompt definition: ${r.status}`); return; }
        const j = await r.json();
        setPromptText(j.prompt ?? '');
        setPromptAgent(j.executor?.agent ?? '');
        setPromptSkills(Array.isArray(j.executor?.skills) ? j.executor.skills : []);
        setPromptLoadedRef(existing.body_ref);
      } catch (e) {
        setErr(e instanceof Error ? e.message : String(e));
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [editing, existing?.body_kind, existing?.body_ref]);

  // Reset body_ref when body_kind switches
  function onBodyKindChange(k: BodyKind) {
    setBodyKind(k);
    setBodyRef('');
    setInput({});
    setPipelineSchema(null);
  }

  function canProceed(): boolean {
    if (step === 1) {
      // prompt body has no body_ref picker — the schedule name doubles as
      // the prompt name, derived at submit time. Let user proceed past
      // Step 1 just by picking the tab.
      if (bodyKind === 'prompt') return true;
      return !!bodyRef;
    }
    if (step === 2) {
      if (bodyKind === 'pipeline') {
        if (!pipelineSchema) return false;
        for (const f of pipelineSchema.input) {
          if (f.required && !input[f.name]?.trim()) return false;
        }
        return true;
      }
      if (bodyKind === 'prompt') {
        // project is optional — empty falls back to the synthetic 'scratch' project
        return !!promptText.trim();
      }
      return false;
    }
    if (step === 3) {
      if (!name.trim()) return false;
      if (trigger === 'cron' && !cron.trim()) return false;
      if (trigger === 'once' && !scheduleAt) return false;
      if (actionKind === 'chat' && !chatSessionId) return false;
      if (actionKind === 'email' && !emailTo.trim()) return false;
      return true;
    }
    return false;
  }

  async function submit() {
    setErr('');
    setSubmitting(true);
    try {
      let body_input: Record<string, unknown>;
      let promptNameToUse: string | null = null;
      if (bodyKind === 'pipeline') {
        body_input = input;
      } else if (bodyKind === 'prompt') {
        // Create/update the prompts/<name>.yaml first; schedule references it by name.
        // In edit mode body_ref is locked and refers to the existing yaml — PATCH it.
        // In create mode derive name from schedule name (slugify) and POST with
        // automatic suffix retry on 409.
        const skillsClean = promptSkills.filter((s) => s && s.trim());
        const executor = { agent: promptAgent || null, skills: skillsClean };
        if (editing && existing?.body_kind === 'prompt') {
          promptNameToUse = existing.body_ref;
          const r = await fetch(`/api/prompts/${encodeURIComponent(existing.body_ref)}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: promptText, executor }),
          });
          if (!r.ok) {
            const j = await r.json().catch(() => ({}));
            setErr(j.error || `prompt yaml save failed (${r.status})`);
            return;
          }
        } else {
          const base = slugifyName(name) || `prompt-${Date.now().toString(36).slice(-6)}`;
          let candidate = base;
          for (let i = 2; i <= 50; i++) {
            const r = await fetch('/api/prompts', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ name: candidate, prompt: promptText, executor }),
            });
            if (r.ok) { promptNameToUse = candidate; break; }
            if (r.status === 409) { candidate = `${base}-${i}`; continue; }
            const j = await r.json().catch(() => ({}));
            setErr(j.error || `prompt yaml create failed (${r.status})`);
            return;
          }
          if (!promptNameToUse) {
            setErr(`couldn't pick a free prompt name starting from "${base}"`);
            return;
          }
        }
        body_input = { project: promptProject };
      } else {
        setErr(`unsupported body_kind: ${bodyKind}`);
        return;
      }
      const action_config: Record<string, unknown> = {};
      if (actionKind === 'chat') {
        action_config.session_id = chatSessionId;
        if (chatPrefix) action_config.prefix = chatPrefix;
      } else if (actionKind === 'email') {
        const to = emailTo.split(/[,;]\s*/).map((s) => s.trim()).filter(Boolean);
        action_config.to = to.length > 1 ? to : (to[0] || '');
        if (emailSubjectTpl) action_config.subject_template = emailSubjectTpl;
        if (emailBodyTpl) action_config.body_template = emailBodyTpl;
      } else if (actionKind === 'telegram') {
        if (telegramChatId.trim()) action_config.chat_id = telegramChatId.trim();
        if (telegramPrefix) action_config.prefix = telegramPrefix;
      }
      const body: any = {
        name: name.trim(),
        input: body_input,
        skills: extraSkills,
        action_kind: actionKind,
        action_config,
        schedule_kind: trigger,
        // Reset stale trigger fields so a kind change wipes the old one.
        schedule_interval_minutes: trigger === 'period' ? interval : null,
        schedule_cron: trigger === 'cron' ? cron.trim() : null,
        schedule_at: trigger === 'once' ? new Date(scheduleAt).toISOString() : null,
      };
      if (!editing) {
        body.body_kind = bodyKind;
        body.body_ref = bodyKind === 'prompt' ? promptNameToUse! : bodyRef;
        body.enabled = true;
      }
      const url = editing ? `/api/schedules/${encodeURIComponent(existing!.id)}` : '/api/schedules';
      const r = await fetch(url, {
        method: editing ? 'PATCH' : 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        setErr(j.error || `HTTP ${r.status}`);
        return;
      }
      onCreated();
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e));
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[12vh]">
      <div className="bg-[var(--bg-primary)] border border-[var(--border)] rounded shadow-2xl w-[680px] max-w-[95vw] max-h-[80vh] flex flex-col">
        {/* Header */}
        <div className="px-4 py-3 border-b border-[var(--border)] flex items-center gap-3">
          <h2 className="text-[13px] font-semibold flex-1">{editing ? `✎ Edit Schedule — ${existing?.name}` : '+ Schedule'}</h2>
          <Stepper step={step} />
          <button onClick={onClose} className="text-[14px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] w-6 h-6">✕</button>
        </div>

        {/* Body */}
        <div className="flex-1 overflow-auto p-4">
          {step === 1 && (
            <Step1
              bodyKind={bodyKind} onBodyKindChange={onBodyKindChange}
              bodyRef={bodyRef} onBodyRefChange={setBodyRef}
              workflows={workflows}
            />
          )}
          {step === 2 && bodyKind === 'pipeline' && pipelineSchema && (
            <Step2Pipeline schema={pipelineSchema} input={input} onChange={setInput} />
          )}
          {step === 2 && bodyKind === 'prompt' && (
            <Step2Prompt
              projects={projects}
              project={promptProject} onProject={setPromptProject}
              promptText={promptText} onPromptText={setPromptText}
              agent={promptAgent} onAgent={setPromptAgent}
              agents={agents}
              skills={promptSkills} onSkills={setPromptSkills}
              allSkills={allSkills}
              editing={editing}
            />
          )}
          {step === 2 && bodyKind === 'pipeline' && (
            <ExtraSkillsSection
              skills={extraSkills}
              onChange={setExtraSkills}
              allSkills={allSkills}
              onOpenPicker={() => setExtraSkillsPickerOpen(true)}
            />
          )}
          {step === 3 && (
            <Step3
              name={name} onName={setName}
              trigger={trigger} onTrigger={setTrigger}
              interval={interval} onInterval={setInterval}
              cron={cron} onCron={setCron}
              scheduleAt={scheduleAt} onScheduleAt={setScheduleAt}
              bodyKind={bodyKind} bodyRef={bodyRef}
              actionKind={actionKind} onActionKind={setActionKind}
              chatSessionId={chatSessionId} onChatSessionId={setChatSessionId}
              chatPrefix={chatPrefix} onChatPrefix={setChatPrefix}
              chatSessions={chatSessions}
              emailTo={emailTo} onEmailTo={setEmailTo}
              emailSubjectTpl={emailSubjectTpl} onEmailSubjectTpl={setEmailSubjectTpl}
              emailBodyTpl={emailBodyTpl} onEmailBodyTpl={setEmailBodyTpl}
              telegramChatId={telegramChatId} onTelegramChatId={setTelegramChatId}
              telegramPrefix={telegramPrefix} onTelegramPrefix={setTelegramPrefix}
            />
          )}
          {err && <div className="text-[11px] text-[var(--red)] bg-[var(--red)]/10 rounded p-2 mt-3">{err}</div>}
        </div>

        {/* Footer */}
        <div className="px-4 py-3 border-t border-[var(--border)] flex items-center justify-between">
          <button onClick={onClose} className="text-[11px] px-3 py-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Cancel</button>
          <div className="flex gap-2">
            {step > 1 && !(editing && step === 2) && (
              <button
                onClick={() => setStep((step - 1) as 1 | 2 | 3)}
                disabled={submitting}
                className="text-[11px] px-3 py-1 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)] disabled:opacity-50"
              >← Back</button>
            )}
            {step < 3 ? (
              <button
                onClick={() => setStep((step + 1) as 1 | 2 | 3)}
                disabled={!canProceed()}
                className="text-[11px] px-3 py-1 bg-[var(--text-primary)] text-[var(--bg-primary)] rounded disabled:opacity-50"
              >Next →</button>
            ) : (
              <button
                onClick={() => void submit()}
                disabled={!canProceed() || submitting}
                className="text-[11px] px-3 py-1 bg-[var(--accent)] text-[var(--bg-primary)] rounded font-semibold disabled:opacity-50"
              >{submitting ? (editing ? 'Saving…' : 'Creating…') : (editing ? 'Save' : 'Create')}</button>
            )}
          </div>
        </div>
      </div>
      {extraSkillsPickerOpen && (
        <MultiSkillPickerModal
          all={allSkills.filter((s) => !s.name.startsWith('_'))}
          selected={extraSkills}
          onClose={() => setExtraSkillsPickerOpen(false)}
          onChange={setExtraSkills}
        />
      )}
    </div>
  );
}

function Stepper({ step }: { step: 1 | 2 | 3 }) {
  return (
    <div className="flex items-center gap-1 text-[10px]">
      {[1, 2, 3].map((n) => (
        <div key={n} className="flex items-center">
          <div className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-semibold ${
            n < step ? 'bg-[var(--accent)] text-[var(--bg-primary)]' :
            n === step ? 'bg-[var(--text-primary)] text-[var(--bg-primary)]' :
            'bg-[var(--bg-secondary)] text-[var(--text-secondary)]'
          }`}>
            {n < step ? '✓' : n}
          </div>
          {n < 3 && <span className={`w-4 h-px ${n < step ? 'bg-[var(--accent)]' : 'bg-[var(--border)]'} mx-0.5`} />}
        </div>
      ))}
    </div>
  );
}

function Step1({
  bodyKind, onBodyKindChange,
  bodyRef, onBodyRefChange,
  workflows,
}: {
  bodyKind: BodyKind; onBodyKindChange: (k: BodyKind) => void;
  bodyRef: string; onBodyRefChange: (n: string) => void;
  workflows: Workflow[];
}) {
  return (
    <div>
      <div className="text-[11px] text-[var(--text-secondary)] mb-2">Step 1 / 3 — Pick what this schedule runs.</div>

      {/* Body kind tabs */}
      <div className="flex gap-1 mb-3 border-b border-[var(--border)]">
        {(['pipeline', 'prompt'] as BodyKind[]).map((k) => (
          <button
            key={k}
            onClick={() => onBodyKindChange(k)}
            className={`text-[11px] px-3 py-1.5 border-b-2 ${
              bodyKind === k
                ? 'border-[var(--text-primary)] font-semibold'
                : 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
            }`}
          >
            {k === 'pipeline' ? '📋 Pipeline' : '💬 Prompt'}
          </button>
        ))}
      </div>

      {/* Body ref list */}
      {bodyKind === 'pipeline' ? (
        workflows.length === 0 ? (
          <div className="text-[11px] text-[var(--text-secondary)]">Loading pipelines…</div>
        ) : (
          <div className="space-y-1.5">
            {workflows.map((w) => (
              <Card key={w.name} title={w.name} desc={w.description}
                    checked={bodyRef === w.name} onPick={() => onBodyRefChange(w.name)} />
            ))}
          </div>
        )
      ) : (
        <div className="text-[11px] text-[var(--text-secondary)] p-3 border border-dashed border-[var(--border)] rounded">
          Prompt mode runs a one-shot Claude task with your prompt text + chosen agent + skills.
          You&apos;ll author the prompt on the next step. The schedule&apos;s name becomes the prompt&apos;s
          filename (<code className="font-mono">prompts/&lt;name&gt;.yaml</code>) so future edits stay
          in one place.
        </div>
      )}
    </div>
  );
}

function Card({ title, desc, checked, onPick }: {
  title: string; desc?: string | null; checked: boolean; onPick: () => void;
}) {
  return (
    <label
      className={`block cursor-pointer rounded border p-2.5 ${
        checked
          ? 'border-[var(--text-primary)] bg-[var(--bg-secondary)]'
          : 'border-[var(--border)] hover:bg-[var(--bg-secondary)]/50'
      }`}
    >
      <div className="flex items-baseline gap-2">
        <input type="radio" name="body-ref" checked={checked} onChange={onPick} />
        <span className="text-[12px] font-mono font-semibold">{title}</span>
      </div>
      {desc && <div className="text-[10px] text-[var(--text-secondary)] mt-1 ml-5">{desc}</div>}
    </label>
  );
}

function Step2Pipeline({ schema, input, onChange }: {
  schema: PipelineSchema; input: Record<string, string>; onChange: (v: Record<string, string>) => void;
}) {
  function update(k: string, v: string) {
    onChange({ ...input, [k]: v });
  }
  return (
    <div>
      <div className="text-[11px] text-[var(--text-secondary)] mb-2">
        Step 2 / 3 — Fill pipeline input. Pipeline: <code className="font-mono">{schema.name}</code>.
      </div>
      {schema.description && (
        <div className="text-[10px] text-[var(--text-secondary)] mb-3 italic">{schema.description}</div>
      )}
      <div className="space-y-3">
        {schema.input.map((f) => (
          <PipelineInputControl
            key={f.name} field={f}
            value={input[f.name] ?? (f.default != null ? String(f.default) : '')}
            onChange={(v) => update(f.name, v)}
          />
        ))}
      </div>
    </div>
  );
}

function PipelineInputControl({ field, value, onChange }: {
  field: PipelineInputField; value: string; onChange: (v: string) => void;
}) {
  const labelEl = (
    <div className="flex items-baseline gap-1 text-[11px] mb-0.5">
      <span className="font-mono">{field.name}</span>
      {field.required && <span className="text-[var(--red)]">*</span>}
      {field.label && <span className="text-[10px] text-[var(--text-secondary)]">— {field.label}</span>}
    </div>
  );
  const descEl = field.description ? (
    <div className="text-[10px] text-[var(--text-secondary)] mt-0.5 whitespace-pre-wrap">{field.description}</div>
  ) : null;

  // enum → select
  if (field.type === 'enum' && Array.isArray(field.enum) && field.enum.length > 0) {
    return (
      <label className="block">
        {labelEl}
        <select
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        >
          <option value="">— pick —</option>
          {field.enum.map((opt) => (
            <option key={opt} value={opt}>{opt}</option>
          ))}
        </select>
        {descEl}
      </label>
    );
  }

  // boolean → checkbox; value is "true"/"false" string (pipeline templates do string substitution)
  if (field.type === 'boolean') {
    const checked = value === 'true' || value === '1';
    return (
      <label className="block">
        {labelEl}
        <label className="flex items-center gap-2 text-[11px]">
          <input
            type="checkbox"
            checked={checked}
            onChange={(e) => onChange(e.target.checked ? 'true' : 'false')}
          />
          <span className="text-[11px] text-[var(--text-secondary)]">{checked ? 'true' : 'false'}</span>
        </label>
        {descEl}
      </label>
    );
  }

  // number / integer
  if (field.type === 'number' || field.type === 'integer') {
    return (
      <label className="block">
        {labelEl}
        <input
          type="number"
          step={field.type === 'integer' ? 1 : 'any'}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        />
        {descEl}
      </label>
    );
  }

  // string (default) — multiline if field declares it OR fallback to heuristic
  const isMultiLine = field.multiline === true ||
    (field.multiline === undefined && (
      /multi-line|multiline|focus|prompt|description|text|body|template/i.test(field.description || '') ||
      /_text$|_focus$|_prompt$|_body$|_template$/.test(field.name)
    ));
  return (
    <label className="block">
      {labelEl}
      {isMultiLine ? (
        <textarea
          rows={3}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        />
      ) : (
        <input
          type="text"
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        />
      )}
      {descEl}
    </label>
  );
}

// Lower-case, alphanumeric + dash. Used to derive the prompts/<name>.yaml
// filename from the schedule's display name. The store's name regex is
// `[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}` so this is a strict subset.
function slugifyName(s: string): string {
  return s.toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 50);
}

function Step2Prompt({
  projects, project, onProject,
  promptText, onPromptText,
  agent, onAgent, agents,
  skills, onSkills, allSkills,
  editing,
}: {
  projects: ProjectInfo[];
  project: string; onProject: (v: string) => void;
  promptText: string; onPromptText: (v: string) => void;
  agent: string; onAgent: (v: string) => void;
  agents: AgentInfo[];
  skills: string[]; onSkills: (v: string[]) => void;
  allSkills: SkillItem[];
  editing: boolean;
}) {
  function toggleSkill(name: string) {
    onSkills(skills.includes(name) ? skills.filter((s) => s !== name) : [...skills, name]);
  }
  // Surface a small curated subset by default; the picker further down
  // lets the user add anything. ai-orchestration is the V3 default so a
  // prompt task can call connectors.
  const featured = ['ai-orchestration', 'temper-memory', 'knowledge-base'];
  const featuredAvailable = featured.filter((n) =>
    skills.includes(n) || allSkills.some((s) => s.name === n)
  );
  const extras = skills.filter((s) => !featured.includes(s));
  return (
    <div>
      <div className="text-[11px] text-[var(--text-secondary)] mb-3">
        Step 2 / 3 — Author the prompt task.{' '}
        {editing && <span>(Editing existing prompt definition.)</span>}
      </div>

      <label className="block mb-3">
        <div className="text-[11px] mb-0.5"><span className="font-mono">project</span></div>
        <select
          value={project}
          onChange={(e) => onProject(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        >
          <option value="">— scratch (default) —</option>
          {projects.filter((p) => p.name !== 'scratch').map((p) => (
            <option key={p.name} value={p.name}>{p.name}</option>
          ))}
        </select>
        <div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
          Working directory for the Claude task. Leave on default for prompts that just talk to connectors / APIs and don&apos;t need a real project on disk.
        </div>
      </label>

      <label className="block mb-3">
        <div className="flex items-baseline gap-1 text-[11px] mb-0.5">
          <span className="font-mono">prompt</span>
          <span className="text-[var(--red)]">*</span>
        </div>
        <textarea
          value={promptText}
          onChange={(e) => onPromptText(e.target.value)}
          rows={8}
          placeholder="e.g. 查一下我今天分到的 bug"
          className="w-full text-[11px] font-mono px-2 py-1.5 border border-[var(--border)] rounded bg-[var(--bg-secondary)] resize-y"
        />
        <div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
          Sent to the agent verbatim. Keep your own wording — Forge won&apos;t rewrite it.
        </div>
      </label>

      <label className="block mb-3">
        <div className="text-[11px] mb-0.5"><span className="font-mono">agent</span></div>
        <select
          value={agent}
          onChange={(e) => onAgent(e.target.value)}
          className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        >
          <option value="">— system default —</option>
          {agents.map((a) => (
            <option key={a.id} value={a.id}>
              {a.label || a.id}{a.backendType ? ` (${a.backendType})` : ''}
            </option>
          ))}
        </select>
      </label>

      <div className="block mb-1">
        <div className="text-[11px] mb-1"><span className="font-mono">skills</span></div>
        <div className="flex flex-wrap gap-1.5">
          {featuredAvailable.map((name) => (
            <label
              key={name}
              className={`text-[10px] font-mono px-2 py-1 border rounded cursor-pointer ${
                skills.includes(name)
                  ? 'border-[var(--accent)] bg-[var(--accent)]/10 text-[var(--accent)]'
                  : 'border-[var(--border)] hover:bg-[var(--bg-secondary)]/50'
              }`}
            >
              <input
                type="checkbox"
                checked={skills.includes(name)}
                onChange={() => toggleSkill(name)}
                className="mr-1"
              />
              {name}
            </label>
          ))}
          {extras.map((name) => (
            <span
              key={name}
              className="text-[10px] font-mono px-2 py-1 border border-[var(--accent)] bg-[var(--accent)]/10 text-[var(--accent)] rounded inline-flex items-center gap-1"
            >
              {name}
              <button
                type="button"
                onClick={() => toggleSkill(name)}
                className="hover:opacity-70"
                aria-label="remove"
              >×</button>
            </span>
          ))}
        </div>
        <div className="text-[10px] text-[var(--text-secondary)] mt-1">
          <code className="font-mono">ai-orchestration</code> lets the agent discover + call your installed connectors.
        </div>
      </div>
    </div>
  );
}


function Step3({
  name, onName,
  trigger, onTrigger,
  interval, onInterval,
  cron, onCron,
  scheduleAt, onScheduleAt,
  bodyKind, bodyRef,
  actionKind, onActionKind,
  chatSessionId, onChatSessionId,
  chatPrefix, onChatPrefix,
  chatSessions,
  emailTo, onEmailTo,
  emailSubjectTpl, onEmailSubjectTpl,
  emailBodyTpl, onEmailBodyTpl,
  telegramChatId, onTelegramChatId,
  telegramPrefix, onTelegramPrefix,
}: {
  name: string; onName: (v: string) => void;
  trigger: 'period' | 'cron' | 'once' | 'manual'; onTrigger: (v: 'period' | 'cron' | 'once' | 'manual') => void;
  interval: number; onInterval: (v: number) => void;
  cron: string; onCron: (v: string) => void;
  scheduleAt: string; onScheduleAt: (v: string) => void;
  bodyKind: BodyKind; bodyRef: string;
  actionKind: 'none' | 'chat' | 'email' | 'telegram'; onActionKind: (v: 'none' | 'chat' | 'email' | 'telegram') => void;
  chatSessionId: string; onChatSessionId: (v: string) => void;
  chatPrefix: string; onChatPrefix: (v: string) => void;
  chatSessions: Array<{ id: string; title: string }>;
  emailTo: string; onEmailTo: (v: string) => void;
  emailSubjectTpl: string; onEmailSubjectTpl: (v: string) => void;
  emailBodyTpl: string; onEmailBodyTpl: (v: string) => void;
  telegramChatId: string; onTelegramChatId: (v: string) => void;
  telegramPrefix: string; onTelegramPrefix: (v: string) => void;
}) {
  return (
    <div>
      <div className="text-[11px] text-[var(--text-secondary)] mb-3">Step 3 / 3 — Name, trigger, action.</div>

      <label className="block mb-3">
        <div className="text-[11px] mb-0.5">Name</div>
        <input
          type="text"
          value={name}
          onChange={(e) => onName(e.target.value)}
          placeholder={`${bodyKind} · ${bodyRef}`}
          className="w-full text-[11px] px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
        />
      </label>

      <div className="text-[11px] mb-1.5">Trigger</div>
      <div className="space-y-1.5 mb-3">
        <Radio label="Every N minutes" v="period" trigger={trigger} onChange={onTrigger}>
          <input
            type="number"
            min={1}
            value={interval}
            onChange={(e) => onInterval(Math.max(1, Number(e.target.value) || 1))}
            disabled={trigger !== 'period'}
            className="text-[11px] font-mono px-2 py-0.5 w-16 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          />
          <span className="text-[11px] text-[var(--text-secondary)]">min</span>
        </Radio>
        <Radio label="Cron" v="cron" trigger={trigger} onChange={onTrigger}>
          <input
            type="text"
            value={cron}
            onChange={(e) => onCron(e.target.value)}
            disabled={trigger !== 'cron'}
            placeholder="*/15 * * * *"
            className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          />
        </Radio>
        <Radio label="Once at" v="once" trigger={trigger} onChange={onTrigger}>
          <input
            type="datetime-local"
            value={scheduleAt}
            onChange={(e) => onScheduleAt(e.target.value)}
            disabled={trigger !== 'once'}
            className="text-[11px] font-mono px-2 py-0.5 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          />
        </Radio>
        <Radio label="Manual only (Fire button)" v="manual" trigger={trigger} onChange={onTrigger}>
          <span className="text-[10px] text-[var(--text-secondary)]">— won't auto-trigger</span>
        </Radio>
      </div>

      <div className="text-[11px] mb-1.5 mt-4">Action (when body finishes)</div>
      <div className="space-y-1.5 mb-3">
        <label className="flex items-center gap-2 cursor-pointer">
          <input type="radio" name="action" checked={actionKind === 'none'} onChange={() => onActionKind('none')} />
          <span className="text-[11px] w-44">None</span>
          <span className="text-[10px] text-[var(--text-secondary)]">— body self-handles output</span>
        </label>
        <label className="flex items-center gap-2 cursor-pointer">
          <input type="radio" name="action" checked={actionKind === 'chat'} onChange={() => onActionKind('chat')} />
          <span className="text-[11px] w-44">Send to Chat session</span>
          <select
            value={chatSessionId}
            onChange={(e) => onChatSessionId(e.target.value)}
            disabled={actionKind !== 'chat'}
            className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          >
            <option value="">— pick session —</option>
            {chatSessions.map((s) => (
              <option key={s.id} value={s.id}>{s.title || s.id.slice(0, 8)}</option>
            ))}
          </select>
        </label>
        {actionKind === 'chat' && (
          <label className="flex items-center gap-2 cursor-pointer">
            <span className="text-[11px] w-44 ml-6">prefix (optional)</span>
            <input
              type="text"
              value={chatPrefix}
              onChange={(e) => onChatPrefix(e.target.value)}
              placeholder="📋 Daily report:\n\n"
              className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
            />
          </label>
        )}
        <label className="flex items-center gap-2 cursor-pointer">
          <input type="radio" name="action" checked={actionKind === 'email'} onChange={() => onActionKind('email')} />
          <span className="text-[11px] w-44">Send Email (SMTP)</span>
          <input
            type="text"
            value={emailTo}
            onChange={(e) => onEmailTo(e.target.value)}
            disabled={actionKind !== 'email'}
            placeholder="to@example.com, …"
            className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          />
        </label>
        {actionKind === 'email' && (
          <>
            <label className="flex items-center gap-2">
              <span className="text-[11px] w-44 ml-6">subject template</span>
              <input
                type="text"
                value={emailSubjectTpl}
                onChange={(e) => onEmailSubjectTpl(e.target.value)}
                placeholder="{date} — Forge schedule"
                className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
              />
            </label>
            <label className="flex items-baseline gap-2">
              <span className="text-[11px] w-44 ml-6">body template</span>
              <textarea
                rows={3}
                value={emailBodyTpl}
                onChange={(e) => onEmailBodyTpl(e.target.value)}
                placeholder="{body_output}"
                className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
              />
            </label>
            <div className="text-[10px] text-[var(--text-secondary)] ml-[12.4rem]">
              Placeholders: {'{date}'} (YYYY-MM-DD) and {'{body_output}'}. SMTP host configured in Settings → SMTP.
            </div>
          </>
        )}
        <label className="flex items-center gap-2 cursor-pointer">
          <input type="radio" name="action" checked={actionKind === 'telegram'} onChange={() => onActionKind('telegram')} />
          <span className="text-[11px] w-44">Send Telegram</span>
          <input
            type="text"
            value={telegramChatId}
            onChange={(e) => onTelegramChatId(e.target.value)}
            disabled={actionKind !== 'telegram'}
            placeholder="chat id (blank = settings.telegramChatId)"
            className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)] disabled:opacity-50"
          />
        </label>
        {actionKind === 'telegram' && (
          <label className="flex items-center gap-2 cursor-pointer">
            <span className="text-[11px] w-44 ml-6">prefix (optional)</span>
            <input
              type="text"
              value={telegramPrefix}
              onChange={(e) => onTelegramPrefix(e.target.value)}
              placeholder="📋 Daily bugs:"
              className="text-[11px] font-mono px-2 py-0.5 flex-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
            />
          </label>
        )}
      </div>
    </div>
  );
}

function Radio({ label, v, trigger, onChange, children }: {
  label: string;
  v: 'period' | 'cron' | 'once' | 'manual';
  trigger: 'period' | 'cron' | 'once' | 'manual';
  onChange: (v: 'period' | 'cron' | 'once' | 'manual') => void;
  children?: React.ReactNode;
}) {
  return (
    <label className="flex items-center gap-2 cursor-pointer">
      <input type="radio" name="trigger" value={v} checked={trigger === v} onChange={() => onChange(v)} />
      <span className="text-[11px] w-44">{label}</span>
      {children}
    </label>
  );
}

// ─── Extra-skills section (Step 2) ─────────────────────────
//
// Multi-select for skills the dispatched body should use, in addition
// to whatever the body itself loads (body=skill's own /skill ref or a
// pipeline node's prompt). Forwarded as --append-system-prompt; Forge
// auto-installs missing skills into the target project at dispatch.

function ExtraSkillsSection({
  skills, onChange, allSkills, onOpenPicker,
}: {
  skills: string[];
  onChange: (v: string[]) => void;
  allSkills: SkillItem[];
  onOpenPicker: () => void;
}) {
  const empty = allSkills.length === 0;
  return (
    <div className="mt-4 pt-3 border-t border-[var(--border)]">
      <div className="flex items-baseline gap-2 mb-1">
        <span className="text-[11px] font-semibold">Skills <span className="text-[var(--text-secondary)] font-normal">(optional)</span></span>
      </div>
      <div className="text-[10px] text-[var(--text-secondary)] mb-1.5 leading-relaxed">
        Forge skills the dispatched task should use. Auto-installed into the
        target project before each run; appended as "use /&lt;skill&gt; as appropriate"
        to the task's system prompt.
      </div>
      <div className="flex items-center gap-1.5 flex-wrap">
        {skills.length === 0 ? (
          <span className="text-[10px] text-[var(--text-secondary)]">(none selected)</span>
        ) : (
          skills.map((name) => (
            <span
              key={name}
              className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] rounded font-mono bg-[var(--accent)]/15 text-[var(--accent)]"
            >
              /{name}
              <button
                type="button"
                onClick={() => onChange(skills.filter((x) => x !== name))}
                title="Remove"
                className="leading-none text-[11px] hover:opacity-70"
              >×</button>
            </span>
          ))
        )}
        <button
          type="button"
          onClick={onOpenPicker}
          disabled={empty}
          title={empty ? 'No skills synced yet — open the Skills tab' : 'Pick skills'}
          className="text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)] disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {empty ? 'No skills synced' : skills.length > 0 ? '+ Add / edit' : '+ Pick skills'}
        </button>
      </div>
    </div>
  );
}

// Centered modal — search box + scrollable list of toggles. Clicking
// outside / Esc closes. Mirrors the extension JobsTab's SkillPicker
// shape so behavior is consistent across surfaces.
function MultiSkillPickerModal({
  all, selected, onClose, onChange,
}: {
  all: SkillItem[];
  selected: string[];
  onClose: () => void;
  onChange: (v: string[]) => void;
}) {
  const [q, setQ] = useState('');
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const filtered = (() => {
    const needle = q.trim().toLowerCase();
    if (!needle) return all;
    return all.filter((s) => {
      const hay = `${s.name} ${s.displayName || ''} ${s.description || ''}`.toLowerCase();
      return hay.includes(needle);
    });
  })();

  function toggle(name: string) {
    onChange(selected.includes(name) ? selected.filter((x) => x !== name) : [...selected, name]);
  }

  return (
    <div
      onClick={onClose}
      className="fixed inset-0 z-[60] flex items-center justify-center bg-black/45"
    >
      <div
        onClick={(e) => e.stopPropagation()}
        className="bg-[var(--bg-primary)] border border-[var(--border)] rounded shadow-2xl w-[520px] max-w-[92vw] max-h-[80vh] flex flex-col"
      >
        <div className="flex items-center px-3 py-2 border-b border-[var(--border)]">
          <div className="text-[12px] font-semibold flex-1">
            Pick skills
            <span className="ml-2 text-[10px] font-normal text-[var(--text-secondary)]">
              {selected.length} of {all.length} selected
            </span>
          </div>
          <button onClick={onClose} className="text-[11px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]">Done</button>
        </div>
        <div className="px-3 py-2 border-b border-[var(--border)]">
          <input
            autoFocus
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="Search by name or description…"
            className="w-full text-[12px] px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
          />
          {selected.length > 0 && (
            <div className="flex items-center gap-1.5 flex-wrap mt-1.5">
              <span className="text-[10px] text-[var(--text-secondary)]">Selected:</span>
              {selected.map((name) => (
                <span key={name} className="px-1.5 py-px text-[10px] rounded font-mono bg-[var(--accent)]/15 text-[var(--accent)]">/{name}</span>
              ))}
              <button
                type="button"
                onClick={() => onChange([])}
                className="ml-auto text-[10px] px-2 py-0.5 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)]"
              >Clear all</button>
            </div>
          )}
        </div>
        <div className="overflow-auto flex-1">
          {filtered.length === 0 ? (
            <div className="text-[11px] text-[var(--text-secondary)] text-center py-6">No skills match "{q}"</div>
          ) : (
            filtered.map((s) => {
              const isSel = selected.includes(s.name);
              return (
                <label
                  key={s.name}
                  onClick={() => toggle(s.name)}
                  className={`flex items-start gap-2 px-3 py-1.5 border-b border-[var(--border)] cursor-pointer hover:bg-[var(--bg-secondary)] ${isSel ? 'bg-[var(--accent)]/10' : ''}`}
                >
                  <input type="checkbox" checked={isSel} readOnly className="mt-1" />
                  <div className="flex-1 min-w-0">
                    <div className="text-[11px] font-mono">{s.name}</div>
                    {s.description && (
                      <div className="text-[10px] text-[var(--text-secondary)] truncate">{s.description}</div>
                    )}
                  </div>
                  {(s.installedGlobal || (s.installedProjects?.length ?? 0) > 0) && (
                    <span className="text-[9px] text-[var(--text-secondary)] mt-0.5">installed</span>
                  )}
                </label>
              );
            })
          )}
        </div>
      </div>
    </div>
  );
}
