import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Plug, RotateCcw, Send } from "lucide-react";
import { api, type ConfigItem, type PlexTestResult } from "../lib/api";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
import Tabs, { usePersistedTab } from "./Tabs";
import { Switch } from "./ui/form";
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
import { LS } from "../lib/storage";

// Admin Configuration page: edit DB-overridable operational settings (registry-driven from the
// backend — adding a key there makes it appear here automatically). Edits are drafted and
// applied together via one Save/Discard bar (consistent with the Settings page); nothing hits
// the server until Save. Group order is fixed where known so logically related settings (e.g.
// Email/SMTP) stay together.
const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "audit", "plex"];

export default function ConfigPanel() {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const q = useQuery({ queryKey: ["admin-config"], queryFn: api.adminConfig });
  const data = q.data;

  const items = useMemo(() => (data ? Object.values(data.groups).flat() : []), [data]);
  const byKey = useMemo(() => Object.fromEntries(items.map((i) => [i.key, i])), [items]);
  // Baseline draft from the server: non-secrets show their effective value; secrets start empty
  // (write-only — we never load them back).
  const baseline = useMemo(() => {
    const m: Record<string, string> = {};
    for (const it of items) m[it.key] = it.secret ? "" : String(it.value ?? "");
    return m;
  }, [items]);

  const [draft, setDraft] = useState<Record<string, string>>({});
  // Secrets the admin marked to reset-to-default on the next Save (can't be expressed by
  // clearing the field, since an empty secret field means "leave unchanged").
  const [secretReset, setSecretReset] = useState<Record<string, boolean>>({});
  const [saveState, setSaveState] = useState<SaveState>("idle");
  // Bumped after our own save so the draft re-seeds from fresh server values; a plain mid-edit
  // refetch does NOT (see the effect below).
  const [reseedToken, setReseedToken] = useState(0);
  // One tab per config group (persisted). Called before the loading guard so hook order is
  // stable; the active id is clamped to a real group at render time once data has loaded.
  const [tab, setTab] = usePersistedTab(LS.configTab);

  // A stable signature of the config SHAPE — changes only when keys are added/removed, not when an
  // unchanged refetch produces a new baseline object. Re-seed the draft on a shape change (initial
  // load, registry change) or an explicit post-save bump — so a refetch that resolves mid-edit
  // (e.g. an invalidation) can no longer clobber the admin's in-progress draft (CB2).
  const dataVersion = useMemo(() => items.map((i) => i.key).join("|"), [items]);
  useEffect(() => {
    setDraft(baseline);
    setSecretReset({});
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dataVersion, reseedToken]);

  const isDirty = (it: ConfigItem): boolean => {
    if (it.secret) return (draft[it.key] ?? "").trim() !== "" || !!secretReset[it.key];
    return (draft[it.key] ?? "") !== (baseline[it.key] ?? "");
  };
  const dirtyKeys = items.filter(isDirty).map((i) => i.key);
  const dirty = dirtyKeys.length > 0;

  async function save() {
    setSaveState("saving");
    try {
      for (const key of dirtyKeys) {
        const it = byKey[key];
        const raw = draft[key] ?? "";
        if (it.secret) {
          if (secretReset[key] && !raw.trim()) await api.resetConfig(key);
          else if (raw.trim()) await api.setConfig(key, raw);
        } else if (it.type === "bool") {
          await api.setConfig(key, raw === "true");
        } else if (raw === "") {
          // allow_empty keys treat a blank as an explicit empty value (e.g. disable smtp_user /
          // youtube_api_proxy) rather than resetting to the env default (AB3).
          if (it.allow_empty) await api.setConfig(key, "");
          else if (it.is_set) await api.resetConfig(key); // cleared → fall back to default
        } else {
          await api.setConfig(key, it.type === "int" ? Number(raw) : raw);
        }
      }
      await qc.invalidateQueries({ queryKey: ["admin-config"] });
      setReseedToken((n) => n + 1); // re-seed the draft from the saved server state
      setSaveState("saved");
      setTimeout(() => setSaveState((s) => (s === "saved" ? "idle" : s)), 2500);
    } catch {
      setSaveState("error");
      notify({ level: "error", message: t("config.saveFailed") });
    }
  }

  function discard() {
    setDraft(baseline);
    setSecretReset({});
    setSaveState("idle");
  }

  const testEmail = useMutation({
    mutationFn: () => api.testEmail(),
    onSuccess: (r) => notify({ level: "success", message: t("config.testSent", { to: r.to }) }),
    onError: () => notify({ level: "error", message: t("config.testFailed") }),
  });

  // Plex "Test connection": verifies the server URL + token and returns the library sections,
  // which drive the library-picker below (checked sections are written into the plex_libraries
  // draft as a comma-separated list of section keys, applied on Save).
  const [plexResult, setPlexResult] = useState<PlexTestResult | null>(null);
  const testPlex = useMutation({
    mutationFn: () => api.testPlex(),
    onSuccess: (r) => {
      setPlexResult(r);
      notify({ level: "success", message: t("config.plexTestOk", { name: r.server_name }) });
    },
    onError: () => {
      setPlexResult(null);
      notify({ level: "error", message: t("config.plexTestFailed") });
    },
  });
  const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
  const togglePlexLib = (key: string) => {
    // An empty list means "all libraries" — every box renders checked. Unchecking one from that
    // state must select all-except-this, so expand "all" to the explicit section set first;
    // otherwise the toggle would ADD the key and leave it as the ONLY selected library.
    const allKeys = plexResult?.sections?.map((s) => s.key) ?? [];
    const current = plexLibs.length === 0 ? allKeys : plexLibs;
    const next = current.includes(key)
      ? current.filter((k) => k !== key)
      : [...current, key];
    setDraft((d) => ({ ...d, plex_libraries: next.join(",") }));
  };

  if (q.isLoading || !data) {
    return <div className="p-4 max-w-3xl mx-auto text-muted">{t("config.loading")}</div>;
  }

  const groupKeys = Object.keys(data.groups).sort(
    (a, b) => (GROUP_ORDER.indexOf(a) + 1 || 99) - (GROUP_ORDER.indexOf(b) + 1 || 99)
  );
  // Clamp the persisted tab to a real group (the registry can change between sessions). The
  // tab pill carries a dot when that group has unsaved edits, so a draft in a hidden tab is
  // never silently lost behind the global Save bar.
  const activeGroup = groupKeys.includes(tab) ? tab : groupKeys[0];
  const dirtyByGroup = new Set(dirtyKeys.map((k) => byKey[k]?.group).filter(Boolean));
  const groupTabs = groupKeys.map((g) => ({
    id: g,
    label: `${t(`config.groups.${g}`, g)}${dirtyByGroup.has(g) ? " •" : ""}`,
  }));

  return (
    <div className="p-4 max-w-3xl w-full mx-auto pb-24">
      <p className="text-xs text-muted mb-4 leading-relaxed">{t("config.intro")}</p>

      <Tabs tabs={groupTabs} active={activeGroup} onChange={setTab} />

      {activeGroup && (
        <div className="glass rounded-2xl p-4 mb-4">
          <div className="divide-y divide-border/60">
            {data.groups[activeGroup].map((item) => (
              <Field
                key={item.key}
                item={item}
                value={draft[item.key] ?? ""}
                secretsManageable={data.secrets_manageable}
                pendingSecretReset={!!secretReset[item.key]}
                onChange={(v) => setDraft((d) => ({ ...d, [item.key]: v }))}
                onResetField={() => {
                  if (item.secret) setSecretReset((s) => ({ ...s, [item.key]: !s[item.key] }));
                  else setDraft((d) => ({ ...d, [item.key]: "" }));
                }}
              />
            ))}
          </div>

          {activeGroup === "email" && (
            <div className="mt-3 pt-3 border-t border-border/60">
              <Tooltip hint={t("config.testEmailHint")}>
                <button
                  onClick={() => testEmail.mutate()}
                  disabled={testEmail.isPending || dirty}
                  className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
                >
                  <Send className={`w-4 h-4 ${testEmail.isPending ? "animate-pulse" : ""}`} />
                  {t("config.testEmail")}
                </button>
              </Tooltip>
            </div>
          )}

          {activeGroup === "plex" && (
            <div className="mt-3 pt-3 border-t border-border/60">
              <Tooltip hint={t("config.plexTestHint")}>
                <button
                  onClick={() => testPlex.mutate()}
                  disabled={testPlex.isPending || dirty}
                  className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
                >
                  <Plug className={`w-4 h-4 ${testPlex.isPending ? "animate-pulse" : ""}`} />
                  {t("config.plexTest")}
                </button>
              </Tooltip>
              {dirty && <p className="text-[11px] text-muted mt-2">{t("config.plexTestDirty")}</p>}
              {plexResult && (
                <div className="mt-3">
                  <p className="text-xs text-muted mb-2">
                    {t("config.plexConnected", {
                      name: plexResult.server_name,
                      version: plexResult.version ?? "",
                    })}
                  </p>
                  {plexResult.media_check?.checked &&
                    (plexResult.media_check.ok ? (
                      <p className="text-[11px] text-emerald-500 mb-2">{t("config.plexMediaOk")}</p>
                    ) : (
                      <p className="text-[11px] text-red-400 mb-2">
                        {t("config.plexMediaMissing", { path: plexResult.media_check.local_path ?? "" })}
                      </p>
                    ))}
                  <p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
                  <div className="flex flex-col gap-1.5">
                    {plexResult.sections.map((s) => (
                      <label
                        key={s.key}
                        className="inline-flex items-center gap-2 text-sm cursor-pointer"
                      >
                        <input
                          type="checkbox"
                          checked={plexLibs.length === 0 || plexLibs.includes(s.key)}
                          onChange={() => togglePlexLib(s.key)}
                          className="accent-accent"
                        />
                        <span>{s.title}</span>
                        <span className="text-[11px] text-muted">
                          {s.type === "movie" ? t("config.plexMovies") : t("config.plexShows")}
                          {s.count != null ? ` · ${s.count}` : ""}
                        </span>
                      </label>
                    ))}
                  </div>
                  <p className="text-[11px] text-muted mt-2">{t("config.plexPickHint")}</p>
                </div>
              )}
            </div>
          )}
        </div>
      )}

      <DraftSaveBar
        variant="floating"
        dirty={dirty}
        state={saveState}
        onSave={save}
        onDiscard={discard}
        labels={{
          saved: t("config.saved"),
          failed: t("config.saveFailed"),
          unsaved: t("config.unsaved"),
          discard: t("config.discard"),
          save: t("config.save"),
          saving: t("config.saving"),
        }}
      />
    </div>
  );
}

function Field({
  item,
  value,
  secretsManageable,
  pendingSecretReset,
  onChange,
  onResetField,
}: {
  item: ConfigItem;
  value: string;
  secretsManageable: boolean;
  pendingSecretReset: boolean;
  onChange: (v: string) => void;
  onResetField: () => void;
}) {
  const { t } = useTranslation();
  const isNum = item.type === "int";
  const isBool = item.type === "bool";
  const secretDisabled = item.secret && !secretsManageable;

  // Status line: never reveal a secret's value.
  let status: string;
  if (item.secret) {
    status = secretDisabled
      ? t("config.secretsDisabled")
      : pendingSecretReset
        ? t("config.willReset")
        : item.is_set
          ? t("config.secretSet")
          : item.default_is_set
            ? t("config.secretEnv")
            : t("config.secretUnset");
  } else {
    status = item.is_set ? t("config.overridden") : t("config.usingDefault");
  }

  // Reset affordance shows when a DB override exists: clears a non-secret to its default, or
  // toggles a secret's reset-on-save (applied on Save, not immediately). A boolean toggle is
  // its own control — storing its value is equivalent to the default, so no reset link.
  const showReset = item.is_set && !isBool;

  return (
    <div className="flex items-start justify-between gap-3 py-3">
      <div className="min-w-0 flex-1">
        <div className="text-sm font-medium">{t(`config.fields.${item.key}.label`, item.key)}</div>
        <p className="text-xs text-muted leading-relaxed mt-0.5">
          {t(`config.fields.${item.key}.hint`, "")}
        </p>
        <p className="text-[11px] text-muted mt-1">{status}</p>
      </div>

      <div className="flex flex-col items-end gap-1.5 shrink-0">
        {isBool ? (
          <Switch
            label={t(`config.fields.${item.key}.label`, item.key)}
            checked={value === "true"}
            onChange={(v) => onChange(v ? "true" : "false")}
          />
        ) : (
          <input
            type={item.secret ? "password" : isNum ? "number" : "text"}
            value={value}
            disabled={secretDisabled || (item.secret && pendingSecretReset)}
            onChange={(e) => onChange(e.target.value)}
            min={isNum && item.min != null ? item.min : undefined}
            max={isNum && item.max != null ? item.max : undefined}
            placeholder={item.secret ? "••••••••" : String(item.default ?? "")}
            aria-label={t(`config.fields.${item.key}.label`, item.key)}
            className="w-52 bg-card border border-border rounded-xl px-3 py-1.5 text-sm outline-none focus:border-accent disabled:opacity-50"
          />
        )}
        {showReset && !secretDisabled && (
          <Tooltip hint={t("config.resetHint")}>
            <button
              onClick={onResetField}
              className={`inline-flex items-center gap-1 text-xs transition ${
                item.secret && pendingSecretReset
                  ? "text-accent"
                  : "text-muted hover:text-fg"
              }`}
            >
              <RotateCcw className="w-3.5 h-3.5" />
              {t("config.reset")}
            </button>
          </Tooltip>
        )}
      </div>
    </div>
  );
}