// components/tasks.jsx — TasksPanel: unified Tickets + Tools view.
// Board with inline job logs, queue controls, QuickDo at top.
// Depends on: React (useState, useCallback, useEffect, useMemo),
//             Card, StatusBadge, get, post, put from api.js,
//             QuickDo from home.jsx,
//             TICKET_STATUSES, STATUS_COLORS, PRIORITY_COLORS, TYPE_ICONS, TOOLS from tickets.jsx,
//             JOB_STATUS_COLORS from tools.jsx

// ── Table Export Component ─────────────────────────────────────

function TableExport({ data, filename, title }) {
  const [showMenu, setShowMenu] = useState(false);

  const convertToCSV = (data) => {
    if (!data || data.length === 0) return '';
    
    const headers = Object.keys(data[0]);
    const csvHeaders = headers.join(',');
    
    const csvRows = data.map(row => {
      return headers.map(header => {
        const value = row[header];
        // Handle nested objects and arrays
        if (typeof value === 'object' && value !== null) {
          return `"${JSON.stringify(value).replaceAll('"', '""')}"`;
        }
        // Escape commas, quotes, and newlines in strings
        if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r'))) {
          return `"${value.replaceAll('"', '""')}"`;
        }
        return value ?? '';
      }).join(',');
    });
    
    return [csvHeaders, ...csvRows].join('\n');
  };

  const downloadCSV = () => {
    const csv = convertToCSV(data);
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const link = document.createElement('a');
    const url = URL.createObjectURL(blob);
    link.setAttribute('href', url);
    link.setAttribute('download', `${filename}.csv`);
    link.style.visibility = 'hidden';
    document.body.appendChild(link);
    link.click();
    link.remove();
  };

  const downloadJSON = () => {
    const json = JSON.stringify(data, null, 2);
    const blob = new Blob([json], { type: 'application/json;charset=utf-8;' });
    const link = document.createElement('a');
    const url = URL.createObjectURL(blob);
    link.setAttribute('href', url);
    link.setAttribute('download', `${filename}.json`);
    link.style.visibility = 'hidden';
    document.body.appendChild(link);
    link.click();
    link.remove();
  };

  const copyToClipboard = (format, event) => {
    let content;
    if (format === 'csv') {
      content = convertToCSV(data);
    } else {
      content = JSON.stringify(data, null, 2);
    }
    
    navigator.clipboard.writeText(content).then(() => {
      // Show brief success feedback
      const button = event.target;
      const originalText = button.textContent;
      button.textContent = 'Copied!';
      button.className = button.className.replace('bg-gray-100', 'bg-green-100').replace('text-gray-600', 'text-green-600');
      setTimeout(() => {
        button.textContent = originalText;
        button.className = button.className.replace('bg-green-100', 'bg-gray-100').replace('text-green-600', 'text-gray-600');
      }, 2000);
    });
  };

  if (!data || data.length === 0) {
    return null;
  }

  return (
    <div className="relative">
      <button
        onClick={() => setShowMenu(!showMenu)}
        className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center gap-1"
        title="Export table data"
      >
        📊 Export ▼
      </button>
      
      {showMenu && (
        <div className="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10">
          <div className="py-1">
            <button
              onClick={() => { downloadCSV(); setShowMenu(false); }}
              className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
            >
              📄 Download CSV
            </button>
            <button
              onClick={() => { downloadJSON(); setShowMenu(false); }}
              className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
            >
              📋 Download JSON
            </button>
            <button
              onClick={(e) => { copyToClipboard('csv', e); setShowMenu(false); }}
              className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
            >
              📋 Copy CSV
            </button>
            <button
              onClick={(e) => { copyToClipboard('json', e); setShowMenu(false); }}
              className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
            >
              📋 Copy JSON
            </button>
          </div>
        </div>
      )}
      
      {/* Close menu when clicking outside */}
      {showMenu && (
        <div
          className="fixed inset-0 z-0"
          onClick={() => setShowMenu(false)}
          onKeyDown={(e) => { if (e.key === 'Escape') setShowMenu(false); }}
          tabIndex={-1}
        />
      )}
    </div>
  );
}

// ── Hook: useTasksData — tickets + queue + tools in one ─────

function useTasksData() {
  const [tickets, setTickets] = useState([]);
  const [sprints, setSprints] = useState([]);
  const [queue, setQueue] = useState(null);
  const [jobs, setJobs] = useState([]);
  const [tools, setTools] = useState([]);
  const [loading, setLoading] = useState(true);

  const refresh = useCallback(async () => {
    setLoading(true);
    const [tData, sData, qData, toolData] = await Promise.all([
      get("/dashboard/tickets"),
      get("/dashboard/sprints"),
      get("/dashboard/tools/queue"),
      get("/dashboard/tools/"),
    ]);
    setTickets(tData?.tickets || []);
    setSprints(sData?.sprints || []);
    setQueue(qData);
    setJobs(qData?.recent_jobs || []);
    setTools(toolData?.tools || []);
    setLoading(false);
  }, []);

  useEffect(() => {
    refresh();
    const timer = setInterval(refresh, 12000);
    return () => clearInterval(timer);
  }, [refresh]);

  return { tickets, sprints, queue, jobs, tools, loading, refresh };
}

// ── TaskCardHeader — title + priority + status badges ────────

function TaskCardHeader({ ticket }) {
  const statusColor = STATUS_COLORS[ticket.status] || "bg-gray-100 text-gray-600";
  const priorityColor = PRIORITY_COLORS[ticket.priority] || "bg-gray-100 text-gray-600";
  const typeIcon = TYPE_ICONS[ticket.type] || "📋";
  return (
    <div className="flex items-start justify-between gap-2">
      <div className="flex items-center gap-1.5 min-w-0">
        <span title={ticket.type}>{typeIcon}</span>
        <span className="font-medium text-gray-900 truncate">{ticket.task || ticket.title}</span>
      </div>
      <div className="flex items-center gap-1 shrink-0">
        <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${priorityColor}`}>{ticket.priority}</span>
        <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${statusColor}`}>{ticket.status?.replace("_", " ")}</span>
      </div>
    </div>
  );
}

// ── TaskCardMeta — ticket id + tool + job execution status ───

function TaskCardMeta({ ticket, ticketJob }) {
  const job = ticketJob || ticket.last_job;
  const resultSuccess = job?.result?.success ?? job?.success;
  const resultLabel = typeof resultSuccess === "boolean"
    ? (resultSuccess ? "success" : "failed")
    : (job ? ((job.status === "queued" || job.status === "running") ? "pending" : "unknown") : null);
  return (
    <div className="mt-1.5 space-y-1">
      <div className="flex items-center gap-2 text-xs text-gray-400">
        <span className="font-mono">{ticket.id?.slice(0, 12)}</span>
        {ticket.assigned_tool && (
          <span className="px-1.5 py-0.5 bg-indigo-50 text-indigo-600 rounded">
            🔧 {ticket.assigned_tool.tool}
          </span>
        )}
      </div>
      {job && (
        <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-gray-500">
          {job.project_dir && (
            <span className="font-mono truncate max-w-[220px]" title={job.project_dir}>
              📁 {job.project_dir}
            </span>
          )}
          {job.prompt_length ? <span>{job.prompt_length} chars prompt</span> : null}
          {resultLabel && (
            <span className={resultLabel === "success" ? "text-emerald-600" : resultLabel === "failed" ? "text-red-600" : "text-gray-500"}>
              tool result: {resultLabel}
            </span>
          )}
        </div>
      )}
      {job && <TaskJobBadge job={job} />}
    </div>
  );
}

// ── TaskJobBadge — compact execution status line ─────────────

function TaskJobBadge({ job }) {
  if (!job) return null;

  const statusIcon = {
    queued: "○", running: "⏳", done: "✅", failed: "❌", cancelled: "⊘"
  }[job.status] || "?";

  const duration = job.duration_s > 0 ? `${Math.round(job.duration_s)}s` : null;
  const filesChanged = job.result?.files_changed;
  const hasOutput = job.result?.stdout;
  const resultSuccess = job.result?.success ?? job.success;
  const resultLabel = typeof resultSuccess === "boolean"
    ? (resultSuccess ? "success" : "failed")
    : (job.status === "queued" || job.status === "running" ? "pending" : "unknown");

  return (
    <div className={`flex items-center gap-1.5 text-[11px] px-2 py-1 rounded ${
      job.status === "done" ? "bg-emerald-50 text-emerald-700" :
      job.status === "failed" ? "bg-red-50 text-red-600" :
      job.status === "running" ? "bg-amber-50 text-amber-700" :
      "bg-gray-50 text-gray-500"
    }`}>
      <span>{statusIcon}</span>
      <span className="font-medium">{job.status}</span>
      {job.tool && <span className="text-gray-400">via {job.tool}</span>}
      {resultLabel && (
        <span className={resultLabel === "success" ? "text-emerald-500" : resultLabel === "failed" ? "text-red-500" : "text-gray-400"}>
          • tool result: {resultLabel}
        </span>
      )}
      {duration && <span className="text-gray-400">• {duration}</span>}
      {filesChanged?.length > 0 && (
        <span className="text-gray-400">• {filesChanged.length} file{filesChanged.length > 1 ? "s" : ""}</span>
      )}
      {job.status === "done" && hasOutput && (
        <span className="text-emerald-500">• has output</span>
      )}
      {job.error && (
        <span className="text-red-400 truncate max-w-[150px]" title={job.error}>• {job.error}</span>
      )}
    </div>
  );
}

// ── TaskInlineLogs — poll + display stdout for running jobs ──

function TaskInlineLogs({ jobId, onDone }) {
  const [logs, setLogs] = useState("");

  useEffect(() => {
    if (!jobId) return;
    const timer = setInterval(async () => {
      const j = await get(`/dashboard/tools/queue/jobs/${jobId}`);
      if (j?.result?.stdout) setLogs(j.result.stdout.slice(-500));
      if (j?.status === "done" || j?.status === "failed") {
        clearInterval(timer);
        if (onDone) onDone();
      }
    }, 3000);
    return () => clearInterval(timer);
  }, [jobId]);

  return (
    <pre className="mt-2 p-2 bg-gray-900 text-green-300 text-[11px] rounded overflow-auto max-h-[120px] font-mono leading-tight">
      {logs || "Waiting for output..."}
    </pre>
  );
}

// ── TaskJobError — show error for failed jobs ────────────────

function TaskJobError({ error }) {
  if (!error) return null;
  return (
    <div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700 truncate">
      {error}
    </div>
  );
}

// ── TaskJobResult — collapsible output for done jobs ─────────

function TaskJobResult({ result }) {
  const [expanded, setExpanded] = useState(false);
  if (!result) return null;
  const output = result.stdout || "";
  const files = result.files_changed || [];
  const preview = output.slice(-200).trim();
  return (
    <div className="mt-2">
      <button onClick={() => setExpanded(!expanded)}
        className="text-[11px] text-emerald-600 hover:text-emerald-800 font-medium">
        {expanded ? "▾ Hide output" : "▸ Show output"}
        {files.length > 0 && ` (${files.length} file${files.length > 1 ? "s" : ""} changed)`}
      </button>
      {expanded && (
        <div className="mt-1 space-y-1">
          {files.length > 0 && (
            <div className="text-[11px] text-gray-500">
              {files.slice(0, 8).map((f, i) => <div key={i} className="font-mono truncate">📄 {f}</div>)}
              {files.length > 8 && <div className="text-gray-400">...+{files.length - 8} more</div>}
            </div>
          )}
          {output && (
            <pre className="p-2 bg-gray-900 text-green-300 text-[11px] rounded overflow-auto max-h-[120px] font-mono leading-tight">
              {preview}
            </pre>
          )}
        </div>
      )}
    </div>
  );
}

// ── TaskStatusActions — expandable status change buttons ─────

function TaskStatusActions({ ticket, onRefresh }) {
  const [expanded, setExpanded] = useState(false);

  const handleStatusChange = async (newStatus) => {
    await put(`/dashboard/tickets/${ticket.id}`, { status: newStatus });
    if (onRefresh) onRefresh();
  };

  return (
    <div>
      <button onClick={() => setExpanded(!expanded)}
        className="mt-1.5 text-xs text-gray-400 hover:text-gray-600">
        {expanded ? "▴ Less" : "▸ Actions"}
      </button>
      {expanded && (
        <div className="mt-2 pt-2 border-t border-gray-100 flex flex-wrap gap-1">
          {TICKET_STATUSES.filter(s => s !== ticket.status).map(s => (
            <button key={s} onClick={() => handleStatusChange(s)}
              className="text-[11px] px-2 py-0.5 rounded border border-gray-200 hover:bg-gray-50">
              {s.replace("_", " ")}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ── TaskTicketCard — thin shell composing sub-components ─────

function TaskTicketCard({ ticket, jobs, onRefresh }) {
  // Prefer real-time job from queue, fallback to server-enriched last_job
  const liveJob = jobs.find(j => j.ticket_id === ticket.id);
  const job = liveJob || ticket.last_job;
  const isActive = job && (job.status === "running" || job.status === "queued");
  const isDone = job?.status === "done";
  const isFailed = job?.status === "failed";

  return (
    <div className={`bg-white rounded-lg border p-3 text-sm transition-shadow hover:shadow-sm ${
      isActive ? "border-amber-300 ring-1 ring-amber-100" :
      isDone ? "border-emerald-200" :
      isFailed ? "border-red-200" :
      "border-gray-200"
    }`}>
      <TaskCardHeader ticket={ticket} />
      <TaskCardMeta ticket={ticket} ticketJob={job} />
      {isActive && <TaskInlineLogs jobId={job.id} onDone={onRefresh} />}
      {isFailed && <TaskJobError error={job.error} />}
      {isDone && job.result?.stdout && <TaskJobResult result={job.result} />}
      <TaskStatusActions ticket={ticket} onRefresh={onRefresh} />
    </div>
  );
}

// ── QueueBar — compact queue controls ────────────────────────

function QueueBar({ queue, onStartStop }) {
  if (!queue) return null;
  const byStatus = queue.by_status || {};
  return (
    <div className="flex items-center gap-4 px-4 py-2 bg-gray-50 rounded-lg border text-sm">
      <span className="font-medium text-gray-700">Queue</span>
      <span className="text-gray-500"><strong>{queue.queue_size || 0}</strong> pending</span>
      {byStatus.running > 0 && <span className="text-amber-600"><strong>{byStatus.running}</strong> running</span>}
      {byStatus.done > 0 && <span className="text-emerald-600"><strong>{byStatus.done}</strong> done</span>}
      {byStatus.failed > 0 && <span className="text-red-500"><strong>{byStatus.failed}</strong> failed</span>}
      <div className="ml-auto flex items-center gap-2">
        <span className={`text-xs font-medium ${queue.running ? "text-emerald-600" : "text-gray-400"}`}>
          {queue.running ? "● Running" : "○ Stopped"}
        </span>
        <button onClick={() => onStartStop(!queue.running)}
          className={`text-xs px-2.5 py-1 rounded font-medium ${
            queue.running
              ? "bg-red-100 text-red-600 hover:bg-red-200"
              : "bg-emerald-100 text-emerald-600 hover:bg-emerald-200"
          }`}>
          {queue.running ? "⏸ Pause" : "▶ Start"}
        </button>
      </div>
    </div>
  );
}

const HUMAN_REQUEST_TYPES = [
  { id: "api_token", label: "API token / secret" },
  { id: "dependency_install", label: "Install dependency" },
  { id: "clickthrough", label: "Click-through / onboarding" },
  { id: "provider_account", label: "Create provider account" },
  { id: "verification", label: "Verify / approve" },
  { id: "other", label: "Other manual step" },
];

function _humanRequestLabel(requestType) {
  return HUMAN_REQUEST_TYPES.find(type => type.id === requestType)?.label || "Manual task";
}

function useHumanTaskData() {
  const [projects, setProjects] = useState([]);
  const [tickets, setTickets] = useState([]);
  const [loading, setLoading] = useState(true);

  const refresh = useCallback(async () => {
    setLoading(true);
    try {
      const [projectsData, ticketsData] = await Promise.all([
        get("/dashboard/projects"),
        get("/dashboard/tickets?tool=human"),
      ]);
      setProjects(Array.isArray(projectsData) ? projectsData : []);
      setTickets(ticketsData?.tickets || []);
    } catch (e) {
      console.error("Failed to load human tasks:", e);
      setProjects([]);
      setTickets([]);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { refresh(); }, [refresh]);

  return { projects, tickets, loading, refresh };
}

function HumanTaskPanel({ onChanged }) {
  const { projects, tickets, loading, refresh } = useHumanTaskData();
  const [submitting, setSubmitting] = useState(false);
  const [noteDrafts, setNoteDrafts] = useState({});
  const [form, setForm] = useState({
    projectId: "",
    requestType: "api_token",
    priority: "medium",
    task: "",
    details: "",
  });

  const humanTickets = tickets.filter(ticket => {
    const assignedTool = ticket.assigned_tool?.tool;
    const tags = Array.isArray(ticket.tags) ? ticket.tags : [];
    return assignedTool === "human" || tags.includes("human");
  });

  const openTickets = humanTickets.filter(ticket => !["done", "cancelled"].includes(ticket.status));

  const projectOptions = projects.map(project => ({
    id: project.id,
    label: project.name || project.id,
    path: project.local_path || project.source_path || "",
  }));

  const updateForm = (key, value) => {
    setForm(prev => ({ ...prev, [key]: value }));
  };

  const syncParent = async () => {
    if (onChanged) {
      await onChanged();
    }
  };

  const submitTask = async () => {
    if (!form.task.trim()) return;
    setSubmitting(true);
    try {
      await post("/dashboard/tickets", {
        task: form.task.trim(),
        description: [
          `Human request type: ${_humanRequestLabel(form.requestType)}`,
          form.details.trim() ? `Details: ${form.details.trim()}` : "",
        ].filter(Boolean).join("\n\n"),
        project_id: form.projectId || null,
        priority: form.priority,
        type: "task",
        tags: ["human", "manual", form.requestType],
        tool: "human",
      });
      setForm({
        projectId: "",
        requestType: "api_token",
        priority: "medium",
        task: "",
        details: "",
      });
      await refresh();
      await syncParent();
    } catch (e) {
      console.error("Failed to create human task:", e);
    } finally {
      setSubmitting(false);
    }
  };

  const addNote = async (ticketId) => {
    const content = (noteDrafts[ticketId] || "").trim();
    if (!content) return;
    try {
      await post(`/dashboard/tickets/${ticketId}/comment`, {
        author: "human",
        content,
      });
      setNoteDrafts(prev => ({ ...prev, [ticketId]: "" }));
      await refresh();
      await syncParent();
    } catch (e) {
      console.error("Failed to add human note:", e);
    }
  };

  const markDone = async (ticketId) => {
    try {
      await put(`/dashboard/tickets/${ticketId}`, { status: "done" });
      await refresh();
      await syncParent();
    } catch (e) {
      console.error("Failed to mark human task done:", e);
    }
  };

  if (loading) {
    return <Card title="Human tasks"><p className="text-gray-400 text-sm">Loading human tasks…</p></Card>;
  }

  return (
    <Card title={`Human tasks (${openTickets.length} open, ${humanTickets.length} total)`}>
      <div className="grid gap-4 lg:grid-cols-[1.1fr_1fr]">
        <div className="space-y-3">
          <p className="text-xs text-gray-500">
            Create manual tickets for API tokens, dependency installs, onboarding clicks, provider account creation, and other steps a human must complete.
          </p>
          <div className="grid gap-2 md:grid-cols-2">
            <select
              value={form.projectId}
              onChange={e => updateForm("projectId", e.target.value)}
              className="rounded-lg border px-3 py-2 text-sm"
            >
              <option value="">No project</option>
              {projectOptions.map(project => (
                <option key={project.id} value={project.id} title={project.path || project.label}>
                  {project.label}{project.path ? ` · ${project.path}` : ""}
                </option>
              ))}
            </select>
            <select
              value={form.requestType}
              onChange={e => updateForm("requestType", e.target.value)}
              className="rounded-lg border px-3 py-2 text-sm"
            >
              {HUMAN_REQUEST_TYPES.map(type => (
                <option key={type.id} value={type.id}>{type.label}</option>
              ))}
            </select>
            <select
              value={form.priority}
              onChange={e => updateForm("priority", e.target.value)}
              className="rounded-lg border px-3 py-2 text-sm"
            >
              <option value="low">low</option>
              <option value="medium">medium</option>
              <option value="high">high</option>
              <option value="critical">critical</option>
            </select>
          </div>
          <input
            value={form.task}
            onChange={e => updateForm("task", e.target.value)}
            placeholder="What should a human do?"
            className="w-full rounded-lg border px-3 py-2 text-sm"
          />
          <textarea
            value={form.details}
            onChange={e => updateForm("details", e.target.value)}
            placeholder="What data, access, or action is needed?"
            rows={4}
            className="w-full rounded-lg border px-3 py-2 text-sm"
          />
          <div className="flex flex-wrap gap-2">
            <button
              type="button"
              onClick={submitTask}
              disabled={submitting || !form.task.trim()}
              className="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
            >
              {submitting ? "Creating…" : "Create human task"}
            </button>
            <button
              type="button"
              onClick={refresh}
              className="rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200"
            >
              Refresh
            </button>
          </div>
        </div>

        <div className="space-y-3">
          {openTickets.length > 0 ? openTickets.map(ticket => {
            const project = projects.find(projectItem => projectItem.id === ticket.project_id) || null;
            const requestType = Array.isArray(ticket.tags)
              ? ticket.tags.find(tag => HUMAN_REQUEST_TYPES.some(type => type.id === tag))
              : null;

            return (
              <div key={ticket.id} className="rounded-xl border border-gray-200 bg-gray-50 p-3 shadow-sm">
                <div className="flex items-start justify-between gap-3">
                  <div className="min-w-0">
                    <div className="text-sm font-medium text-gray-900 truncate" title={ticket.task || ticket.title}>{ticket.task || ticket.title}</div>
                    <div className="mt-1 text-xs text-gray-500">
                      {project ? `${project.name || project.id}${project.local_path || project.source_path ? ` · ${project.local_path || project.source_path}` : ""}` : "No project"}
                    </div>
                    <div className="mt-1 flex flex-wrap gap-1 text-[11px]">
                      <span className="rounded-full bg-blue-100 px-2 py-0.5 text-blue-700">human</span>
                      {requestType && (
                        <span className="rounded-full bg-gray-100 px-2 py-0.5 text-gray-600">{_humanRequestLabel(requestType)}</span>
                      )}
                      <span className="rounded-full bg-amber-100 px-2 py-0.5 text-amber-700">{ticket.priority}</span>
                      <span className="rounded-full bg-purple-100 px-2 py-0.5 text-purple-700">{ticket.status}</span>
                    </div>
                  </div>
                  <button
                    type="button"
                    onClick={() => markDone(ticket.id)}
                    className="shrink-0 rounded-lg bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700 hover:bg-emerald-200"
                  >
                    Mark done
                  </button>
                </div>

                {ticket.description && (
                  <div className="mt-2 whitespace-pre-wrap rounded-lg bg-white px-3 py-2 text-xs text-gray-600 border border-gray-100">
                    {ticket.description}
                  </div>
                )}

                <div className="mt-2 space-y-2">
                  <textarea
                    value={noteDrafts[ticket.id] || ""}
                    onChange={e => setNoteDrafts(prev => ({ ...prev, [ticket.id]: e.target.value }))}
                    placeholder="Add human update, missing token, install note, or approval status..."
                    rows={3}
                    className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs"
                  />
                  <div className="flex gap-2">
                    <button
                      type="button"
                      onClick={() => addNote(ticket.id)}
                      disabled={!(noteDrafts[ticket.id] || "").trim()}
                      className="rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200 disabled:opacity-50"
                    >
                      Add note
                    </button>
                  </div>
                </div>
              </div>
            );
          }) : (
            <div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-4 text-sm text-gray-400">
              No open human tasks yet. Create one above when a manual step, token, install, or account setup is needed.
            </div>
          )}
        </div>
      </div>
    </Card>
  );
}

// ── TasksPanel (main) ────────────────────────────────────────

function TasksPanel() {
  const { tickets, queue, jobs, loading, refresh } = useTasksData();
  const [view, setView] = useState("board"); // board | list
  const [filterStatus, setFilterStatus] = useState("");

  const filtered = React.useMemo(() => {
    if (!filterStatus) return tickets;
    return tickets.filter(t => t.status === filterStatus);
  }, [tickets, filterStatus]);

  const boardColumns = React.useMemo(() => {
    const cols = {};
    // Simplified board columns: Backlog, To Do, In Progress, Done, Failed
    const boardStatuses = ["backlog", "todo", "in_progress", "in_review", "done", "cancelled"];
    boardStatuses.forEach(s => { cols[s] = []; });
    filtered.forEach(t => {
      if (cols[t.status]) cols[t.status].push(t);
    });
    return cols;
  }, [filtered]);

  const handleStartStop = async (start) => {
    await post(`/dashboard/tools/queue/${start ? "start" : "stop"}`, {});
    refresh();
  };

  if (loading) {
    return <Card title="Tasks"><p className="text-gray-400 text-sm">Loading…</p></Card>;
  }

  return (
    <div className="space-y-4">
      {/* Quick Action */}
      <QuickDo compact onCreated={refresh} />

      <HumanTaskPanel onChanged={refresh} />

      {/* Queue controls */}
      <QueueBar queue={queue} onStartStop={handleStartStop} />

      {/* Toolbar */}
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <div className="flex border rounded overflow-hidden">
            <button onClick={() => setView("board")}
              className={`px-3 py-1 text-xs font-medium ${view === "board" ? "bg-blue-600 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`}>
              Board
            </button>
            <button onClick={() => setView("list")}
              className={`px-3 py-1 text-xs font-medium ${view === "list" ? "bg-blue-600 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`}>
              List
            </button>
          </div>
          <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}
            className="text-xs px-2 py-1 border rounded text-gray-600">
            <option value="">All statuses</option>
            {TICKET_STATUSES.map(s => <option key={s} value={s}>{s.replace("_", " ")}</option>)}
          </select>
          <span className="text-xs text-gray-400">{filtered.length} task{filtered.length !== 1 ? "s" : ""}</span>
        </div>
        <button onClick={refresh} className="text-xs px-3 py-1 bg-gray-100 text-gray-600 rounded hover:bg-gray-200">
          ↻ Refresh
        </button>
      </div>

      {/* Board view */}
      {view === "board" && (
        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
          {TICKET_STATUSES.map(status => (
            <div key={status} className="bg-gray-50 rounded-lg p-2 min-h-[200px]">
              <h5 className={`text-xs font-medium px-2 py-1 rounded mb-2 ${STATUS_COLORS[status] || "bg-gray-100 text-gray-600"}`}>
                {status.replace("_", " ")} ({boardColumns[status]?.length || 0})
              </h5>
              <div className="space-y-2">
                {(boardColumns[status] || []).map(t => (
                  <TaskTicketCard key={t.id} ticket={t} jobs={jobs} onRefresh={refresh} />
                ))}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* List view */}
      {view === "list" && (
        <div className="space-y-2">
          {filtered.length === 0 ? (
            <Card title="No tasks"><p className="text-gray-400 text-sm">Use Quick Action above to create your first task.</p></Card>
          ) : (
            filtered.map(t => (
              <TaskTicketCard key={t.id} ticket={t} jobs={jobs} onRefresh={refresh} />
            ))
          )}
        </div>
      )}

      {/* Recent jobs table (collapsed) */}
      {jobs.length > 0 && (
        <Card title={`Recent Jobs (${jobs.length})`}>
          <div className="flex justify-between items-center mb-2">
            <span className="text-xs text-gray-500">Recent jobs ({jobs.length})</span>
            <TableExport 
              data={jobs.map(job => ({
                id: job.id,
                ticket: job.ticket_id,
                ticket_title: job.ticket?.title || job.ticket?.task || '',
                ticket_status: job.ticket?.status || '',
                ticket_priority: job.ticket?.priority || '',
                ticket_type: job.ticket?.type || '',
                ticket_project_id: job.ticket?.project_id || '',
                ticket_assigned_tool: job.ticket?.assigned_tool?.tool || '',
                tool: job.tool,
                mode: job.mode || '',
                status: job.status,
                duration: job.duration_s > 0 ? `${job.duration_s}s` : '—',
                exit_code: job.result?.exit_code ?? '',
                stdout_lines: job.result?.stdout_lines ?? 0,
                stderr_lines: job.result?.stderr_lines ?? 0,
                stdout: job.result?.stdout || '',
                stderr: job.result?.stderr || '',
                files_changed: Array.isArray(job.result?.files_changed) ? job.result.files_changed.join(', ') : '',
                error: job.error || ''
              }))}
              filename="recent_jobs"
              title="Recent Jobs"
            />
          </div>
          <div className="overflow-x-auto">
            <table className="w-full text-left text-sm">
              <thead>
                <tr className="border-b border-gray-200 text-xs text-gray-500 uppercase">
                  <th className="py-2 px-3">ID</th>
                  <th className="py-2 px-3">Ticket</th>
                  <th className="py-2 px-3">Tool</th>
                  <th className="py-2 px-3">Status</th>
                  <th className="py-2 px-3">Duration</th>
                  <th className="py-2 px-3">Error</th>
                </tr>
              </thead>
              <tbody>
                {jobs.map(j => (
                  <tr key={j.id} className="border-b border-gray-100 hover:bg-gray-50">
                    <td className="py-1.5 px-3 text-xs font-mono text-gray-400">{j.id?.slice(0, 10)}</td>
                    <td className="py-1.5 px-3 text-xs font-mono text-gray-500">{j.ticket_id?.slice(0, 12)}</td>
                    <td className="py-1.5 px-3 text-xs font-medium">{j.tool}</td>
                    <td className="py-1.5 px-3">
                      <span className={`text-[11px] px-1.5 py-0.5 rounded-full ${JOB_STATUS_COLORS[j.status] || "bg-gray-100"}`}>{j.status}</span>
                    </td>
                    <td className="py-1.5 px-3 text-xs text-gray-400">{j.duration_s > 0 ? `${j.duration_s}s` : "—"}</td>
                    <td className="py-1.5 px-3 text-xs text-red-400 truncate max-w-[200px]">{j.error || ""}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </Card>
      )}
    </div>
  );
}
