"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import { apiFetch } from "@/lib/api";

// Hand-rolled types — replace with codegen once backend job_settings endpoints
// appear in /openapi.json (run `npm run codegen` then delete this block).
interface JobSettingRead {
  job_type: string;
  enabled: boolean;
  queue_name: string;
  cron_schedule: string | null;
  rate_ms: number | null;
  max_retries: number;
  max_concurrency: number;
  extra_config: Record<string, unknown>;
  updated_at: string;
  updated_by: string | null;
  description: string | null;
}

interface JobSettingUpdate {
  enabled?: boolean | null;
  queue_name?: string | null;
  cron_schedule?: string | null;
  rate_ms?: number | null;
  max_retries?: number | null;
  max_concurrency?: number | null;
  extra_config?: Record<string, unknown> | null;
}

// Per-row draft state (mirrors JobSettingRead fields that are editable)
interface RowDraft {
  enabled: boolean;
  cron_schedule: string;
  rate_ms: string; // kept as string for controlled input, parsed on save
  max_retries: string;
  max_concurrency: string;
  extra_config_text: string; // raw textarea JSON
  extra_config_error: string | null;
}

interface Toast {
  id: number;
  message: string;
  kind: "success" | "error";
}

function toRowDraft(s: JobSettingRead): RowDraft {
  return {
    enabled: s.enabled,
    cron_schedule: s.cron_schedule ?? "",
    rate_ms: s.rate_ms != null ? String(s.rate_ms) : "",
    max_retries: String(s.max_retries),
    max_concurrency: String(s.max_concurrency),
    extra_config_text: JSON.stringify(s.extra_config, null, 2),
    extra_config_error: null,
  };
}

function formatIso(s: string): string {
  return new Date(s).toLocaleString("ru-RU");
}

export function JobSettingsPanel() {
  const [drafts, setDrafts] = useState<Record<string, RowDraft>>({});
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
  const [toasts, setToasts] = useState<Toast[]>([]);
  // Track in-flight saves per job_type — единственная updateMutation хранит
  // только variables последнего mutate, поэтому при параллельных Save
  // спиннер/opacity рассинхронизировались (issue #1429). Ведём множество.
  const [pendingJobTypes, setPendingJobTypes] = useState<Set<string>>(
    () => new Set<string>(),
  );
  const toastSeqRef = useRef(0);
  const queryClient = useQueryClient();

  const addToast = useCallback((message: string, kind: "success" | "error") => {
    const id = ++toastSeqRef.current;
    setToasts((prev: Toast[]) => [...prev, { id, message, kind }]);
    window.setTimeout(
      () =>
        setToasts((prev: Toast[]) => prev.filter((t: Toast) => t.id !== id)),
      4000,
    );
  }, []);

  const settings = useQuery({
    queryKey: ["job-settings"],
    queryFn: () => apiFetch<JobSettingRead[]>("/api/v1/admin/jobs/settings"),
  });

  // Initialise drafts when settings data first arrives (or new job_types appear).
  // Using useEffect for syncing external data -> local draft state.
  useEffect(() => {
    if (!settings.data) return;
    setDrafts((prev: Record<string, RowDraft>) => {
      const next = { ...prev };
      let changed = false;
      for (const s of settings.data) {
        if (!(s.job_type in next)) {
          next[s.job_type] = toRowDraft(s);
          changed = true;
        }
      }
      return changed ? next : prev;
    });
  }, [settings.data]);

  const updateMutation = useMutation({
    mutationFn: ({
      job_type,
      update,
    }: {
      job_type: string;
      update: JobSettingUpdate;
    }) =>
      apiFetch<JobSettingRead>(`/api/v1/admin/jobs/settings/${job_type}`, {
        method: "PUT",
        body: JSON.stringify(update),
      }),
    onSuccess: (data: JobSettingRead) => {
      setDrafts((prev: Record<string, RowDraft>) => ({
        ...prev,
        [data.job_type]: toRowDraft(data),
      }));
      void queryClient.invalidateQueries({ queryKey: ["job-settings"] });
      addToast(`${data.job_type}: сохранено`, "success");
    },
    onError: (
      err: unknown,
      variables: { job_type: string; update: JobSettingUpdate },
    ) => {
      addToast(
        `${variables.job_type}: ошибка — ${(err as Error).message}`,
        "error",
      );
    },
    onSettled: (
      _data: JobSettingRead | undefined,
      _err: unknown,
      variables: { job_type: string; update: JobSettingUpdate },
    ) => {
      setPendingJobTypes((prev: Set<string>) => {
        if (!prev.has(variables.job_type)) return prev;
        const next = new Set(prev);
        next.delete(variables.job_type);
        return next;
      });
    },
  });

  const patchDraft = (job_type: string, patch: Partial<RowDraft>) => {
    setDrafts((prev: Record<string, RowDraft>) => ({
      ...prev,
      [job_type]: { ...(prev[job_type] ?? {}), ...patch } as RowDraft,
    }));
  };

  const handleSave = (job_type: string) => {
    const draft = drafts[job_type];
    if (!draft) return;

    // Compare draft JSON-text против серверного значения, чтобы не терять правки,
    // когда textarea свёрнута (issue #1239: при collapsed expanded[job_type]
    // парсинг пропускался, PUT уходил без extra_config, бэкенд оставлял старое
    // значение, onSuccess перетирал draft.extra_config_text).
    const serverRow = settings.data?.find(
      (s: JobSettingRead) => s.job_type === job_type,
    );
    const serverExtraConfigText = serverRow
      ? JSON.stringify(serverRow.extra_config, null, 2)
      : null;
    const extraConfigDirty =
      serverExtraConfigText !== null &&
      draft.extra_config_text !== serverExtraConfigText;

    let extra_config: Record<string, unknown> | undefined;
    if (extraConfigDirty) {
      try {
        const parsed = JSON.parse(draft.extra_config_text) as unknown;
        if (
          typeof parsed !== "object" ||
          parsed === null ||
          Array.isArray(parsed)
        ) {
          patchDraft(job_type, {
            extra_config_error: "Должен быть JSON-объект (не массив и не null)",
          });
          // Раскрыть строку, иначе пользователь не увидит сообщение об ошибке.
          setExpanded((prev: Record<string, boolean>) => ({
            ...prev,
            [job_type]: true,
          }));
          return;
        }
        extra_config = parsed as Record<string, unknown>;
        patchDraft(job_type, { extra_config_error: null });
      } catch (e) {
        patchDraft(job_type, {
          extra_config_error: `Невалидный JSON: ${(e as Error).message}`,
        });
        setExpanded((prev: Record<string, boolean>) => ({
          ...prev,
          [job_type]: true,
        }));
        return;
      }
    }

    const update: JobSettingUpdate = {
      enabled: draft.enabled,
      cron_schedule: draft.cron_schedule.trim() || null,
      rate_ms: draft.rate_ms !== "" ? Number(draft.rate_ms) : null,
      max_retries: Number(draft.max_retries),
    };
    // Очистка max_concurrency давала Number("")===0, а backend требует ge=1 →
    // 422 (issue #1430). При пустом поле не включаем его в update — сервер
    // оставит прежнее значение, onSuccess вернёт его в draft.
    if (draft.max_concurrency !== "") {
      update.max_concurrency = Number(draft.max_concurrency);
    }
    if (extra_config !== undefined) {
      update.extra_config = extra_config;
    }

    setPendingJobTypes((prev: Set<string>) => {
      if (prev.has(job_type)) return prev;
      const next = new Set(prev);
      next.add(job_type);
      return next;
    });
    updateMutation.mutate({ job_type, update });
  };

  const isPendingFor = (job_type: string) => pendingJobTypes.has(job_type);

  return (
    <>
      {/* Toast notifications */}
      <div
        style={{
          position: "fixed",
          top: 20,
          right: 20,
          zIndex: 9999,
          display: "flex",
          flexDirection: "column",
          gap: 8,
        }}
      >
        {toasts.map((t: Toast) => (
          <div
            key={t.id}
            style={{
              padding: "10px 16px",
              borderRadius: 8,
              fontSize: 13,
              fontWeight: 500,
              background: t.kind === "success" ? "#f0fdf4" : "#fef2f2",
              border: `1px solid ${t.kind === "success" ? "#bbf7d0" : "#fecaca"}`,
              color: t.kind === "success" ? "#065f46" : "#991b1b",
              boxShadow: "0 2px 8px rgba(0,0,0,.08)",
              maxWidth: 360,
              wordBreak: "break-word",
            }}
          >
            {t.kind === "success" ? "✓ " : "✕ "}
            {t.message}
          </div>
        ))}
      </div>

      <section style={cardStyle}>
        <h3 style={sectionTitle}>Настройки jobs</h3>
        <p style={{ color: "#5b6066", fontSize: 13, marginTop: 0 }}>
          Централизованные настройки Celery jobs. Изменения применяются с
          следующего beat-цикла.
        </p>
        {settings.isLoading ? (
          <p style={{ color: "#5b6066" }}>Загрузка…</p>
        ) : settings.isError ? (
          <p style={{ color: "#b3261e" }}>Ошибка: {String(settings.error)}</p>
        ) : (
          <div style={{ overflowX: "auto" }}>
            <table
              style={{
                width: "100%",
                borderCollapse: "collapse",
                fontSize: 13,
              }}
            >
              <thead>
                <tr style={{ background: "#f6f7f9" }}>
                  {[
                    "job_type",
                    "enabled",
                    "queue_name",
                    "cron_schedule",
                    "rate_ms",
                    "max_retries",
                    "max_concurrency",
                    "extra_config",
                    "updated_at",
                    "",
                  ].map((h) => (
                    <th key={h} style={th}>
                      {h}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {(settings.data ?? []).map((row: JobSettingRead) => {
                  const draft = drafts[row.job_type];
                  if (!draft) return null;
                  const isExpanded = expanded[row.job_type] ?? false;
                  const saving = isPendingFor(row.job_type);

                  return (
                    <tr
                      key={row.job_type}
                      style={{
                        borderBottom: "1px solid #eef0f3",
                        verticalAlign: "top",
                        opacity: saving ? 0.6 : 1,
                        transition: "opacity .15s",
                      }}
                    >
                      {/* job_type — read-only */}
                      <td style={{ ...td, fontWeight: 600 }}>
                        <code
                          style={{
                            fontFamily: "ui-monospace,SFMono-Regular,monospace",
                            background: "#f3f4f6",
                            padding: "2px 6px",
                            borderRadius: 4,
                          }}
                        >
                          {row.job_type}
                        </code>
                        {row.description && (
                          <div
                            style={{
                              fontSize: 11,
                              color: "#6b7280",
                              marginTop: 4,
                              maxWidth: 160,
                            }}
                          >
                            {row.description}
                          </div>
                        )}
                      </td>

                      {/* enabled — checkbox */}
                      <td style={{ ...td, textAlign: "center" }}>
                        <input
                          type="checkbox"
                          checked={draft.enabled}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            patchDraft(row.job_type, {
                              enabled: e.target.checked,
                            })
                          }
                          style={{ width: 16, height: 16, cursor: "pointer" }}
                        />
                      </td>

                      {/* queue_name — read-only */}
                      <td style={{ ...td, color: "#6b7280" }}>
                        <code style={{ fontSize: 12 }}>{row.queue_name}</code>
                      </td>

                      {/* cron_schedule */}
                      <td style={td}>
                        <input
                          type="text"
                          value={draft.cron_schedule}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            patchDraft(row.job_type, {
                              cron_schedule: e.target.value,
                            })
                          }
                          placeholder="* * * * *"
                          style={{
                            ...compactInput,
                            fontFamily: "ui-monospace,SFMono-Regular,monospace",
                            width: 130,
                          }}
                        />
                      </td>

                      {/* rate_ms */}
                      <td style={td}>
                        <input
                          type="number"
                          value={draft.rate_ms}
                          min={0}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            patchDraft(row.job_type, {
                              rate_ms: e.target.value,
                            })
                          }
                          placeholder="—"
                          style={{ ...compactInput, width: 80 }}
                        />
                      </td>

                      {/* max_retries */}
                      <td style={td}>
                        <input
                          type="number"
                          value={draft.max_retries}
                          min={0}
                          max={10}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            patchDraft(row.job_type, {
                              max_retries: e.target.value,
                            })
                          }
                          style={{ ...compactInput, width: 60 }}
                        />
                      </td>

                      {/* max_concurrency */}
                      <td style={td}>
                        <input
                          type="number"
                          value={draft.max_concurrency}
                          min={1}
                          max={20}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            patchDraft(row.job_type, {
                              max_concurrency: e.target.value,
                            })
                          }
                          style={{ ...compactInput, width: 60 }}
                        />
                      </td>

                      {/* extra_config — collapsible textarea */}
                      <td style={td}>
                        <button
                          type="button"
                          onClick={() =>
                            setExpanded((prev: Record<string, boolean>) => ({
                              ...prev,
                              [row.job_type]: !isExpanded,
                            }))
                          }
                          style={toggleBtn}
                        >
                          {isExpanded ? "▼ скрыть" : "▶ JSON"}
                        </button>
                        {isExpanded && (
                          <div style={{ marginTop: 6 }}>
                            <textarea
                              value={draft.extra_config_text}
                              onChange={(
                                e: React.ChangeEvent<HTMLTextAreaElement>,
                              ) => {
                                patchDraft(row.job_type, {
                                  extra_config_text: e.target.value,
                                  extra_config_error: null,
                                });
                              }}
                              rows={6}
                              style={{
                                width: 280,
                                fontFamily:
                                  "ui-monospace,SFMono-Regular,monospace",
                                fontSize: 11,
                                padding: "6px 8px",
                                border: draft.extra_config_error
                                  ? "1px solid #fca5a5"
                                  : "1px solid #d1d5db",
                                borderRadius: 6,
                                resize: "vertical",
                                boxSizing: "border-box",
                              }}
                              spellCheck={false}
                            />
                            {draft.extra_config_error && (
                              <div
                                style={{
                                  color: "#b3261e",
                                  fontSize: 11,
                                  marginTop: 2,
                                }}
                              >
                                {draft.extra_config_error}
                              </div>
                            )}
                          </div>
                        )}
                      </td>

                      {/* updated_at — read-only */}
                      <td
                        style={{
                          ...td,
                          color: "#6b7280",
                          fontSize: 11,
                          whiteSpace: "nowrap",
                        }}
                      >
                        {formatIso(row.updated_at)}
                        {row.updated_by && (
                          <div style={{ color: "#9ca3af" }}>
                            {row.updated_by}
                          </div>
                        )}
                      </td>

                      {/* Save button */}
                      <td style={{ ...td, whiteSpace: "nowrap" }}>
                        <button
                          type="button"
                          onClick={() => handleSave(row.job_type)}
                          disabled={saving}
                          style={
                            saving ? { ...saveBtn, opacity: 0.6 } : saveBtn
                          }
                        >
                          {saving ? "…" : "Сохранить"}
                        </button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </section>
    </>
  );
}

// ── Styles ──────────────────────────────────────────────────────────────────

const cardStyle = {
  background: "#fff",
  border: "1px solid #e6e8ec",
  borderRadius: 12,
  padding: 20,
};

const sectionTitle = {
  margin: "0 0 8px",
  fontSize: 16,
  fontWeight: 600 as const,
};

const compactInput = {
  padding: "6px 8px",
  border: "1px solid #d1d5db",
  borderRadius: 6,
  fontSize: 13,
  boxSizing: "border-box" as const,
};

const th = {
  padding: "8px 10px",
  textAlign: "left" as const,
  fontWeight: 600 as const,
  borderBottom: "1px solid #e6e8ec",
  whiteSpace: "nowrap" as const,
};

const td = { padding: "8px 10px" };

const saveBtn = {
  padding: "6px 14px",
  background: "#1d4ed8",
  color: "#fff",
  border: "none",
  borderRadius: 6,
  fontSize: 13,
  fontWeight: 600 as const,
  cursor: "pointer",
};

const toggleBtn = {
  padding: "3px 8px",
  background: "#f3f4f6",
  color: "#374151",
  border: "1px solid #d1d5db",
  borderRadius: 4,
  fontSize: 11,
  cursor: "pointer",
  whiteSpace: "nowrap" as const,
};
