// src/pages/AdminTickets.jsx (FINAL - sort by updated_at DESC + attachments + audio record/reply)
// ✅ ترتیب تیکت‌ها بر اساس updated_at DESC
// ✅ نمایش attachment داخل چت (audio player / image preview / download card)
// ✅ Reply: ارسال فایل + ضبط صدا (5 دقیقه) + محدودیت حجم 5MB
// ✅ MediaRecorder flush loop + requestData قبل stop (برای جلوگیری از فایل خالی)
// ✅ Toast/ReactQuery UI حفظ شد

import React, { useEffect, useMemo, useState, useCallback, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { createPortal } from "react-dom";
import PageShell from "./_pageShell";
import { api } from "../services/api";
import { useToast } from "../components/ToastProvider";
import DropdownPortal from "../components/DropdownPortal";
import {
  TbRefresh,
  TbSearch,
  TbCheck,
  TbX,
  TbAlertTriangle,
  TbMessageCircle,
  TbFlag,
  TbSend,
  TbPaperclip,
  TbEye,
  TbUser,
  TbClock,
  TbMicrophone,
  TbPlayerStop,
  TbTrash,
} from "react-icons/tb";
import { LuExternalLink } from "react-icons/lu";

/* ---------------- helpers ---------------- */
function cn(...a) {
  return a.filter(Boolean).join(" ");
}
function toFa(v) {
  try {
    return String(v).toLocaleString("fa-IR");
  } catch {
    return String(v ?? "");
  }
}
function fmtDate(iso) {
  if (!iso) return "-";
  try {
    return new Date(iso).toLocaleString("fa-IR", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return iso;
  }
}
const getErrMsg = (e, fallback = "خطای ناشناخته") => {
  const d = e?.response?.data;
  if (!d) return e?.message || fallback;
  if (typeof d === "string") return d;
  if (d.detail) return d.detail;
  if (d.message) return d.message;
  if (Array.isArray(d.body) && d.body[0]) return d.body[0];
  if (Array.isArray(d.attachment) && d.attachment[0]) return d.attachment[0];
  try {
    return JSON.stringify(d);
  } catch {
    return fallback;
  }
};

// ✅ safe date to ms for sorting
function toMs(iso) {
  if (!iso) return 0;
  const t = new Date(iso).getTime();
  return Number.isFinite(t) ? t : 0;
}

const bytesToHuman = (bytes) => {
  if (!Number.isFinite(bytes)) return "-";
  const units = ["B", "KB", "MB", "GB"];
  let b = bytes;
  let i = 0;
  while (b >= 1024 && i < units.length - 1) {
    b /= 1024;
    i++;
  }
  return `${b.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
};
function formatTime(sec) {
  const s = Math.max(0, Math.floor(sec));
  const mm = String(Math.floor(s / 60)).padStart(2, "0");
  const ss = String(s % 60).padStart(2, "0");
  return `${mm}:${ss}`;
}

/* ---------------- limits ---------------- */
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024; // 5MB
const MAX_RECORD_SECONDS = 5 * 60; // 5 دقیقه
const MIN_AUDIO_BYTES = 1200;

/* ---------------- attachment helpers ---------------- */
function getFileNameFromUrl(url = "") {
  try {
    const u = new URL(url);
    const last = u.pathname.split("/").filter(Boolean).pop();
    return decodeURIComponent(last || "attachment");
  } catch {
    const parts = String(url).split("/").filter(Boolean);
    return decodeURIComponent(parts[parts.length - 1] || "attachment");
  }
}
function getExt(name = "") {
  const n = String(name).toLowerCase();
  const i = n.lastIndexOf(".");
  return i >= 0 ? n.slice(i + 1) : "";
}
function guessKindFromUrl(url = "") {
  const name = getFileNameFromUrl(url);
  const ext = getExt(name);
  const audioExt = ["webm", "ogg", "mp3", "wav", "m4a", "aac", "flac", "opus"];
  const imgExt = ["png", "jpg", "jpeg", "webp", "gif", "bmp", "svg"];
  if (audioExt.includes(ext)) return "audio";
  if (imgExt.includes(ext)) return "image";
  if (ext === "pdf") return "pdf";
  return "file";
}
function AttachmentBubble({ url }) {
  if (!url) return null;
  const kind = guessKindFromUrl(url);
  const name = getFileNameFromUrl(url);

  if (kind === "audio") {
    return (
      <div className="mt-2 rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 p-2">
        <div className="text-[11px] text-black/60 dark:text-white/60 mb-2 truncate flex items-center justify-between gap-2">
          <span className="truncate">{name}</span>
          <a
            href={url}
            target="_blank"
            rel="noreferrer"
            className="inline-flex items-center gap-1 text-[11px] font-bold text-[#0b7bd6] dark:text-[#43BEFF]"
            title="باز کردن/دانلود"
          >
            <LuExternalLink className="w-3.5 h-3.5" />
            دانلود
          </a>
        </div>
        <audio controls preload="metadata" src={url} className="w-full" />
      </div>
    );
  }

  if (kind === "image") {
    return (
      <div className="mt-2 rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 p-2">
        <a href={url} target="_blank" rel="noreferrer" className="block">
          <img
            src={url}
            alt={name}
            className="w-full max-h-[260px] object-contain rounded-lg border border-black/10 dark:border-white/10 bg-white/50 dark:bg-white/5"
            loading="lazy"
          />
        </a>
        <div className="mt-2 flex items-center justify-between gap-2">
          <div className="text-[11px] text-black/60 dark:text-white/60 truncate">{name}</div>
          <a
            href={url}
            target="_blank"
            rel="noreferrer"
            className="inline-flex items-center gap-1 text-[11px] font-bold text-[#0b7bd6] dark:text-[#43BEFF]"
          >
            <LuExternalLink className="w-3.5 h-3.5" />
            باز کردن
          </a>
        </div>
      </div>
    );
  }

  return (
    <div className="mt-2 rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 p-3 flex items-center justify-between gap-3">
      <div className="min-w-0">
        <div className="text-xs font-bold text-black dark:text-white truncate">{name}</div>
        <div className="text-[11px] text-black/60 dark:text-white/60">{kind === "pdf" ? "PDF" : "فایل"}</div>
      </div>
      <a
        href={url}
        target="_blank"
        rel="noreferrer"
        className="px-3 py-2 rounded-xl text-xs font-bold bg-white/70 dark:bg-white/10 border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/15 transition inline-flex items-center gap-2"
      >
        <LuExternalLink className="w-4 h-4" />
        دانلود
      </a>
    </div>
  );
}

/* ---------------- Audio Recorder helpers ---------------- */
function pickBestRecorderMime() {
  const MR = window.MediaRecorder;
  if (!MR?.isTypeSupported) return { mimeType: "", ext: "webm" };
  const list = [
    { mt: "audio/webm;codecs=opus", ext: "webm" },
    { mt: "audio/webm", ext: "webm" },
    { mt: "audio/ogg;codecs=opus", ext: "ogg" },
    { mt: "audio/ogg", ext: "ogg" },
  ];
  for (const it of list) {
    try {
      if (MR.isTypeSupported(it.mt)) return { mimeType: it.mt, ext: it.ext };
    } catch {}
  }
  return { mimeType: "", ext: "webm" };
}

/* ---------------- UI atoms ---------------- */
const CARD =
  "rounded-2xl shadow border border-black/10 dark:border-white/10 bg-white/70 dark:bg-white/5 backdrop-blur-sm";
const SOFT =
  "rounded-2xl shadow border border-black/10 dark:border-white/10 bg-[#A1E3F910] dark:bg-[#A1E3F919]";
const INPUT_BASE =
  "w-full dark:bg-[#A1E3F919] bg-[#A1E3F980] text-sm rounded-xl shadow grid relative items-center border border-black/10 dark:border-white/10";
const INPUT =
  "h-11 px-4 w-full outline-0 focus:outline-0 rounded-xl bg-transparent text-black dark:text-white placeholder:text-black/50 dark:placeholder:text-white/50";

function Badge({ tone = "default", children }) {
  const m = {
    default:
      "bg-black/5 border-black/10 text-black/70 dark:text-white/70 dark:bg-white/10 dark:border-white/10",
    success:
      "bg-emerald-500/10 border-emerald-500/25 text-emerald-700 dark:text-emerald-200",
    danger:
      "bg-red-500/10 border-red-500/25 text-red-700 dark:text-red-200",
    warn:
      "bg-amber-500/10 border-amber-500/25 text-amber-700 dark:text-amber-200",
    info:
      "bg-[#43BEFF14] border-[#43BEFF40] text-[#146E85] dark:text-[#A1E3F9]",
  };
  return (
    <span
      className={cn(
        "inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs border",
        m[tone] || m.default
      )}
    >
      {children}
    </span>
  );
}

/* ---------------- Modal ---------------- */
function useBodyScrollLock(locked) {
  useEffect(() => {
    if (!locked) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => (document.body.style.overflow = prev);
  }, [locked]);
}

function Modal({ open, title, onClose, children, widthClass = "max-w-2xl" }) {
  useBodyScrollLock(open);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => e.key === "Escape" && onClose?.();
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  if (!open) return null;

  return createPortal(
    <div className="fixed inset-0 z-[9998]">
      <div className="absolute inset-0 bg-black/45" onClick={onClose} />
      <div className={cn("absolute inset-x-0 top-10 mx-auto px-3", widthClass)}>
        <div className="rounded-2xl shadow-xl border border-black/10 dark:border-white/10 bg-white dark:bg-[#0b1d39] overflow-hidden">
          <div className="px-4 py-3 border-b border-black/10 dark:border-white/10 flex items-center justify-between">
            <div className="font-extrabold text-black dark:text-white text-sm truncate">
              {title}
            </div>
            <button
              type="button"
              onClick={onClose}
              className="rounded-xl px-3 py-2 hover:bg-black/5 dark:hover:bg-white/10 transition"
              disabled={false}
            >
              <TbX className="w-5 h-5" />
            </button>
          </div>
          <div className="p-4">{children}</div>
        </div>
      </div>
    </div>,
    document.body
  );
}

function Field({ label, children, hint }) {
  return (
    <div>
      <div className="mb-1 text-xs text-black/60 dark:text-white/70 font-bold">
        {label}
      </div>
      {children}
      {hint ? (
        <div className="mt-1 text-[11px] text-black/50 dark:text-white/60">
          {hint}
        </div>
      ) : null}
    </div>
  );
}

/* ---------------- API ---------------- */
async function listAdminTickets(params) {
  const res = await api.get("/api/admin/tickets/", { params });
  return res.data;
}
async function getTicket(ticket_id) {
  const res = await api.get(`/api/tickets/${ticket_id}/`);
  return res.data;
}
async function replyTicket({ ticket_id, body, new_status, file }) {
  // همیشه multipart می‌فرستیم (ساده‌تر/سازگارتر)
  const fd = new FormData();
  fd.append("body", body || "");
  if (new_status) fd.append("new_status", new_status);
  if (file) fd.append("attachment", file);

  const res = await api.post(`/api/tickets/${ticket_id}/reply/`, fd, {
    // ⚠️ اگر api شما axios هست، بهتره Content-Type رو ست نکنیم
    // ولی چون کد قبلی شما ست کرده بود، اینجا حذفش کردم تا boundary درست باشه.
  });
  return res.data;
}

/* ---------------- Page ---------------- */
export default function AdminTickets() {
  const qc = useQueryClient();
  const { success, error: toastError, info, warning } = useToast();

  const [ddMenu, setDdMenu] = useState({ id: null, rect: null });

  // filters
  const [q, setQ] = useState("");
  const [statusFilter, setStatusFilter] = useState("all");

  // paging
  const [page, setPage] = useState(1);
  const pageSize = 6;

  // modal + current
  const [detailsOpen, setDetailsOpen] = useState(false);
  const [currentTicketId, setCurrentTicketId] = useState(null);

  // reply draft
  const [replyBody, setReplyBody] = useState("");
  const [replyStatus, setReplyStatus] = useState(""); // optional
  const [replyFile, setReplyFile] = useState(null);
  const fileRef = useRef(null);

  // recorder for reply
  const canRecord =
    typeof window !== "undefined" &&
    !!navigator.mediaDevices?.getUserMedia &&
    !!window.MediaRecorder;

  const mediaStreamRef = useRef(null);
  const mediaRecorderRef = useRef(null);
  const chunksRef = useRef([]);
  const recordTickRef = useRef(null);
  const flushRef = useRef(null);
  const stopRequestedRef = useRef(false);

  const [isRecording, setIsRecording] = useState(false);
  const [recordSec, setRecordSec] = useState(0);

  const statusOptions = useMemo(
    () => [
      { value: "open", label: "Open" },
      { value: "pending", label: "Pending" },
      { value: "answered", label: "Answered" },
      { value: "closed", label: "Closed" },
    ],
    []
  );
  const statusFilterOptions = useMemo(
    () => [{ value: "all", label: "همه" }, ...statusOptions],
    [statusOptions]
  );

  const statusTone = useCallback((s) => {
    const v = String(s || "").toLowerCase();
    if (v === "closed") return { tone: "success", label: "Closed" };
    if (v === "answered") return { tone: "info", label: "Answered" };
    if (v === "pending") return { tone: "warn", label: "Pending" };
    if (v === "open") return { tone: "default", label: "Open" };
    return { tone: "default", label: v || "-" };
  }, []);

  const listParams = useCallback(
    (p) => ({
      page: p,
      page_size: pageSize,
      q: q || undefined,
      status: statusFilter === "all" ? undefined : statusFilter,
    }),
    [pageSize, q, statusFilter]
  );

  const qTickets = useQuery({
    queryKey: ["admin-tickets", { page, pageSize, q, statusFilter }],
    queryFn: () => listAdminTickets(listParams(page)),
    staleTime: 5_000,
    refetchOnWindowFocus: false,
    keepPreviousData: true,
  });

  const tickets = useMemo(() => {
    const d = qTickets.data;
    const arr = Array.isArray(d) ? d : d?.results || [];
    return [...arr].sort((a, b) => {
      const ta = toMs(a?.updated_at) || toMs(a?.created_at);
      const tb = toMs(b?.updated_at) || toMs(b?.created_at);
      return tb - ta;
    });
  }, [qTickets.data]);

  const totalCount = useMemo(() => {
    const d = qTickets.data;
    const c = d?.count;
    return Number.isFinite(Number(c)) ? Number(c) : tickets.length;
  }, [qTickets.data, tickets.length]);

  const totalPages = useMemo(
    () => Math.max(1, Math.ceil(totalCount / pageSize)),
    [totalCount, pageSize]
  );

  useEffect(() => {
    if (page > totalPages) setPage(1);
  }, [page, totalPages]);

  const hasNextPage = useMemo(() => !!qTickets.data?.next, [qTickets.data]);
  const hasPrevPage = useMemo(
    () => !!qTickets.data?.previous && page > 1,
    [qTickets.data, page]
  );

  useEffect(() => {
    const d = qTickets.data;
    if (!d) return;

    if (d?.next) {
      qc.prefetchQuery({
        queryKey: ["admin-tickets", { page: page + 1, pageSize, q, statusFilter }],
        queryFn: () => listAdminTickets(listParams(page + 1)),
        staleTime: 5_000,
      });
    }

    if (d?.previous && page > 1) {
      qc.prefetchQuery({
        queryKey: ["admin-tickets", { page: page - 1, pageSize, q, statusFilter }],
        queryFn: () => listAdminTickets(listParams(page - 1)),
        staleTime: 5_000,
      });
    }
  }, [qTickets.data, qc, page, pageSize, q, statusFilter, listParams]);

  const qDetail = useQuery({
    enabled: detailsOpen && !!currentTicketId,
    queryKey: ["ticket-detail", currentTicketId],
    queryFn: () => getTicket(currentTicketId),
    staleTime: 0,
    refetchOnWindowFocus: false,
  });
  const detail = qDetail.data || null;

  const refreshAll = async () => {
    info("در حال بروزرسانی...", { title: "Tickets" });
    await Promise.allSettled([qTickets.refetch()]);
    success("بروزرسانی شد ✅", { title: "Tickets" });
  };

  const stopTick = () => {
    if (recordTickRef.current) {
      clearInterval(recordTickRef.current);
      recordTickRef.current = null;
    }
  };
  const stopFlush = () => {
    if (flushRef.current) {
      clearInterval(flushRef.current);
      flushRef.current = null;
    }
  };
  const cleanupStreamOnly = () => {
    if (mediaStreamRef.current) {
      try {
        mediaStreamRef.current.getTracks?.().forEach((t) => t.stop());
      } catch {}
    }
    mediaStreamRef.current = null;
  };
  const discardRecording = () => {
    stopTick();
    stopFlush();
    stopRequestedRef.current = false;

    try {
      const mr = mediaRecorderRef.current;
      if (mr && mr.state !== "inactive") {
        try { mr.stop(); } catch {}
      }
    } catch {}

    mediaRecorderRef.current = null;
    chunksRef.current = [];
    cleanupStreamOnly();
    setIsRecording(false);
    setRecordSec(0);
  };

  useEffect(() => {
    return () => discardRecording();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const setReplyFileWithLimit = (f) => {
    if (!f) {
      setReplyFile(null);
      return true;
    }
    if (f.size > MAX_UPLOAD_BYTES) {
      warning(`حجم فایل بیشتر از ۵ مگابایت است (${bytesToHuman(f.size)}).`, { title: "محدودیت حجم" });
      return false;
    }
    setReplyFile(f);
    return true;
  };

  const startRecording = async () => {
    if (!canRecord) {
      warning("مرورگر شما امکان ضبط صدا را پشتیبانی نمی‌کند.", { title: "ضبط صدا" });
      return;
    }
    if (isRecording) return;

    // فایل قبلی حذف (اختیاری)
    if (replyFile) setReplyFile(null);

    stopRequestedRef.current = false;
    chunksRef.current = [];

    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
      });
      mediaStreamRef.current = stream;

      const { mimeType, ext } = pickBestRecorderMime();
      const mr = new MediaRecorder(
        stream,
        mimeType ? { mimeType, audioBitsPerSecond: 128000 } : { audioBitsPerSecond: 128000 }
      );

      mediaRecorderRef.current = mr;
      setRecordSec(0);
      setIsRecording(true);

      mr.ondataavailable = (ev) => {
        if (ev.data && ev.data.size > 0) chunksRef.current.push(ev.data);
      };
      mr.onerror = () => toastError("خطا در ضبط صدا", { title: "ضبط صدا" });

      mr.onstop = async () => {
        stopTick();
        stopFlush();
        setIsRecording(false);

        await new Promise((r) => setTimeout(r, 120));

        const chunks = chunksRef.current || [];
        chunksRef.current = [];

        const typeGuess =
          mr.mimeType ||
          chunks.find((c) => c?.type)?.type ||
          (ext === "ogg" ? "audio/ogg" : "audio/webm");

        const blob = new Blob(chunks, { type: typeGuess });

        if (!blob || blob.size < MIN_AUDIO_BYTES) {
          cleanupStreamOnly();
          warning("فایل ضبط‌شده خالی بود یا صدا ضبط نشد. دوباره ضبط کن.", { title: "ضبط صدا" });
          return;
        }

        if (blob.size > MAX_UPLOAD_BYTES) {
          cleanupStreamOnly();
          warning(`حجم صدای ضبط‌شده بیشتر از ۵ مگابایت شد (${bytesToHuman(blob.size)}).`, { title: "محدودیت حجم" });
          return;
        }

        const safeExt = ext || (typeGuess.includes("ogg") ? "ogg" : "webm");
        const fileName = `admin-voice-${new Date().toISOString().replace(/[:.]/g, "-")}.${safeExt}`;
        const f = new File([blob], fileName, { type: blob.type || typeGuess });

        setReplyFileWithLimit(f);
        success("صدای ضبط‌شده به پیوست اضافه شد ✅", { title: "ضبط صدا" });
        cleanupStreamOnly();
      };

      mr.start();

      flushRef.current = setInterval(() => {
        try {
          const rec = mediaRecorderRef.current;
          if (!rec || rec.state !== "recording") return;
          rec.requestData?.();
        } catch {}
      }, 800);

      recordTickRef.current = setInterval(() => {
        setRecordSec((s) => {
          const next = s + 1;
          if (next >= MAX_RECORD_SECONDS) {
            try {
              info("حداکثر زمان ضبط (۵ دقیقه) رسید. ضبط متوقف شد.", { title: "ضبط صدا" });
              stopRecording();
            } catch {}
          }
          return next;
        });
      }, 1000);

      info("ضبط شروع شد (حداکثر ۵ دقیقه).", { title: "ضبط صدا" });
    } catch (e) {
      discardRecording();
      toastError(getErrMsg(e, "اجازه میکروفون داده نشد یا خطا رخ داد"), { title: "ضبط صدا" });
    }
  };

  const stopRecording = () => {
    if (!isRecording) return;
    const mr = mediaRecorderRef.current;
    if (!mr) return;

    if (stopRequestedRef.current) return;
    stopRequestedRef.current = true;

    try {
      try { mr.requestData?.(); } catch {}
      setTimeout(() => {
        try {
          if (mr.state !== "inactive") mr.stop();
        } catch {
          discardRecording();
        }
      }, 150);
    } catch {
      discardRecording();
    }
  };

  const openDetails = (id) => {
    setDdMenu({ id: null, rect: null });
    setCurrentTicketId(id);
    setReplyBody("");
    setReplyFile(null);
    setReplyStatus("");
    discardRecording();
    setDetailsOpen(true);
  };

  const closeDetails = () => {
    setDetailsOpen(false);
    setCurrentTicketId(null);
    setReplyBody("");
    setReplyFile(null);
    setReplyStatus("");
    discardRecording();
  };

  const mutReply = useMutation({
    mutationFn: replyTicket,
    onSuccess: async () => {
      success("پاسخ ارسال شد ✅", { title: "Ticket" });
      setReplyBody("");
      setReplyFile(null);
      discardRecording();
      if (fileRef.current) fileRef.current.value = "";
      await Promise.allSettled([
        qc.invalidateQueries({ queryKey: ["ticket-detail", currentTicketId] }),
        qc.invalidateQueries({ queryKey: ["admin-tickets"] }),
      ]);
      await qDetail.refetch();
    },
    onError: (e) => toastError(getErrMsg(e, "خطا در ارسال پاسخ"), { title: "Ticket" }),
  });

  const submitReply = () => {
    if (!currentTicketId) return;
    const b = String(replyBody || "").trim();
    if (!b) return toastError("متن پیام اجباری است.", { title: "Ticket" });
    if (isRecording) return warning("اول ضبط صدا را متوقف کن.", { title: "ضبط صدا" });
    if (replyFile && replyFile.size > MAX_UPLOAD_BYTES)
      return warning("حجم پیوست بیشتر از ۵MB است.", { title: "محدودیت حجم" });

    info("در حال ارسال...", { title: "Ticket" });
    mutReply.mutate({
      ticket_id: currentTicketId,
      body: b,
      new_status: replyStatus || undefined,
      file: replyFile || undefined,
    });
  };

  return (
    <PageShell title="مدیریت تیکت‌ها">
      <div className="w-full dark:text-white">
        {/* header */}
        <div className={cn(CARD, "p-3 sm:p-4 mb-4 !bg-[#A1E3F910] dark:!bg-[#A1E3F910]")}>
          <div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
            <div className="min-w-0">
              <h1 className="text-lg sm:text-xl font-extrabold inline-flex items-center gap-2">
                <TbMessageCircle className="w-5 h-5 opacity-80" />
                مدیریت تیکت‌ها
              </h1>
              <p className="text-xs sm:text-sm text-black/60 dark:text-white/70 truncate">
                مدیریت و پاسخ‌دهی به تیکت های کاربران
              </p>
            </div>

            <div className="flex items-center gap-2 w-full lg:w-auto">
              <button
                type="button"
                onClick={refreshAll}
                disabled={qTickets.isFetching}
                className="flex-1 sm:flex-none px-4 py-2 rounded-xl shadow bg-white dark:bg-[#16325b]
                          border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10
                          text-sm inline-flex items-center justify-center gap-2 transition disabled:opacity-60"
              >
                <TbRefresh
                  className={cn("w-4 h-4 text-[#43BEFF] dark:text-white", qTickets.isFetching ? "animate-spin" : "")}
                />
                بروزرسانی
              </button>

              <Badge tone="info">
                تعداد: <b className="font-extrabold">{toFa(totalCount)}</b>
              </Badge>
            </div>
          </div>

          {qTickets.error ? (
            <div className={cn("mt-3 rounded-2xl border border-amber-500/25 bg-amber-500/10 p-3")}>
              <div className="flex items-start gap-2">
                <TbAlertTriangle className="w-5 h-5 mt-0.5" />
                <div className="text-sm">
                  <div className="font-extrabold">خطا در دریافت تیکت‌ها</div>
                  <div className="text-xs opacity-80 mt-1">{getErrMsg(qTickets.error)}</div>
                </div>
              </div>
            </div>
          ) : null}
        </div>

        <div className={cn(CARD, "p-3 sm:p-4 mb-4")} onPointerDown={(e) => e.stopPropagation()}>
          {/* filters */}
          <div className="grid lg:grid-cols-[1fr_240px_220px] gap-2">
            <div className={cn(INPUT_BASE)}>
              <div className="absolute right-3 top-1/2 -translate-y-1/2 opacity-70">
                <TbSearch className="w-5 h-5" />
              </div>
              <input
                className={cn(INPUT, "pr-11")}
                placeholder="جستجو (subject / owner_email / ...)"
                value={q}
                onChange={(e) => {
                  setQ(e.target.value);
                  setPage(1);
                }}
              />
            </div>

            <DropdownPortal
              id="ticket-status-filter"
              menu={ddMenu}
              setMenu={setDdMenu}
              label="وضعیت"
              value={statusFilter}
              options={statusFilterOptions}
              onChange={(v) => {
                setStatusFilter(v);
                setPage(1);
              }}
              width={240}
            />

            <div className={cn(SOFT, "p-1 flex items-center justify-between")}>
              <span className="text-xs text-black/55 dark:text-white/60">
                صفحه: <b>{toFa(page)}</b> / <b>{toFa(totalPages)}</b>
              </span>

              <div className="flex items-center gap-2">
                <button
                  type="button"
                  disabled={!hasPrevPage}
                  onMouseEnter={() => {
                    if (!hasPrevPage) return;
                    qc.prefetchQuery({
                      queryKey: ["admin-tickets", { page: page - 1, pageSize, q, statusFilter }],
                      queryFn: () => listAdminTickets(listParams(page - 1)),
                      staleTime: 5_000,
                    });
                  }}
                  onClick={() => setPage((p) => Math.max(1, p - 1))}
                  className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition text-xs disabled:opacity-50"
                >
                  قبلی
                </button>

                <button
                  type="button"
                  disabled={!hasNextPage}
                  onMouseEnter={() => {
                    if (!hasNextPage) return;
                    qc.prefetchQuery({
                      queryKey: ["admin-tickets", { page: page + 1, pageSize, q, statusFilter }],
                      queryFn: () => listAdminTickets(listParams(page + 1)),
                      staleTime: 5_000,
                    });
                  }}
                  onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
                  className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition text-xs disabled:opacity-50"
                >
                  بعدی
                </button>
              </div>
            </div>
          </div>
        </div>

        {/* list */}
        <div className={cn(CARD, "p-3 sm:p-4")}>
          {qTickets.isLoading ? (
            <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
              {[...Array(9)].map((_, i) => (
                <div key={i} className={cn(SOFT, "p-4 animate-pulse")}>
                  <div className="h-5 w-40 rounded bg-black/5 dark:bg-white/10" />
                  <div className="mt-3 h-4 w-56 rounded bg-black/5 dark:bg-white/10" />
                  <div className="mt-3 h-10 w-full rounded bg-black/5 dark:bg-white/10" />
                </div>
              ))}
            </div>
          ) : tickets.length === 0 ? (
            <div className="py-12 text-center text-black/50 dark:text-white/60">تیکتی برای نمایش وجود ندارد.</div>
          ) : (
            <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
              {tickets.map((t) => {
                const st = statusTone(t?.status);
                const subject = t?.subject || "-";
                const email = t?.owner_email || "-";
                const created = fmtDate(t?.created_at);
                const updated = fmtDate(t?.updated_at);

                return (
                  <div
                    key={String(t.id)}
                    role="button"
                    tabIndex={0}
                    onClick={() => openDetails(t.id)}
                    onKeyDown={(e) => e.key === "Enter" && openDetails(t.id)}
                    className={cn(
                      "group rounded-3xl border border-black/10 dark:border-white/10",
                      "bg-white/70 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/10 transition",
                      "p-4 cursor-pointer min-w-0"
                    )}
                  >
                    <div className="flex items-start justify-between gap-3">
                      <div className="min-w-0">
                        <div className="text-[11px] text-black/55 dark:text-white/60">Ticket #{toFa(t.id)}</div>
                        <div className="mt-1 font-extrabold text-black dark:text-white truncate">{subject}</div>
                      </div>

                      <Badge tone={st.tone}>
                        <TbFlag className="w-4 h-4" />
                        {st.label}
                      </Badge>
                    </div>

                    <div className="mt-3 flex items-center gap-2 text-[12px] text-black/60 dark:text-white/70 min-w-0">
                      <TbUser className="w-4 h-4 opacity-80" />
                      <span className="truncate">{email}</span>
                    </div>

                    <div className="mt-3 grid grid-cols-2 gap-2">
                      <div className={cn(SOFT, "p-2 rounded-2xl")}>
                        <div className="text-[10px] text-black/50 dark:text-white/60 flex items-center gap-1">
                          <TbClock className="w-4 h-4" />
                          created
                        </div>
                        <div className="text-[11px] mt-1 font-mono text-black/70 dark:text-white/70 truncate">{created}</div>
                      </div>
                      <div className={cn(SOFT, "p-2 rounded-2xl")}>
                        <div className="text-[10px] text-black/50 dark:text-white/60 flex items-center gap-1">
                          <TbClock className="w-4 h-4" />
                          updated
                        </div>
                        <div className="text-[11px] mt-1 font-mono text-black/70 dark:text-white/70 truncate">{updated}</div>
                      </div>
                    </div>

                    <div className="mt-4 flex items-center justify-end">
                      <button
                        type="button"
                        onClick={(e) => {
                          e.stopPropagation();
                          openDetails(t.id);
                        }}
                        className={cn(
                          "px-4 py-2 rounded-2xl shadow-sm border border-black/10 dark:border-white/10",
                          "bg-white dark:bg-[#16325b] hover:bg-black/5 dark:hover:bg-white/10 transition",
                          "text-xs inline-flex items-center gap-2"
                        )}
                      >
                        <TbEye className="w-4 h-4 text-[#43BEFF] dark:text-white" />
                        باز کردن
                      </button>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>

        {/* ---------------- Details Modal ---------------- */}
        <Modal
          open={detailsOpen}
          title={currentTicketId ? `تیکت #${toFa(currentTicketId)} — مشاهده و پاسخ` : "تیکت"}
          onClose={() => !mutReply.isPending && closeDetails()}
          widthClass="max-w-4xl"
        >
          {qDetail.isLoading ? (
            <div className={cn(SOFT, "p-4 animate-pulse")}>
              <div className="h-5 w-56 rounded bg-black/5 dark:bg-white/10" />
              <div className="mt-2 h-4 w-72 rounded bg-black/5 dark:bg-white/10" />
              <div className="mt-6 h-32 w-full rounded bg-black/5 dark:bg-white/10" />
            </div>
          ) : qDetail.error ? (
            <div className={cn("rounded-2xl border border-amber-500/25 bg-amber-500/10 p-3")}>
              <div className="flex items-start gap-2">
                <TbAlertTriangle className="w-5 h-5 mt-0.5" />
                <div className="text-sm">
                  <div className="font-extrabold">خطا در دریافت تیکت</div>
                  <div className="text-xs opacity-80 mt-1">{getErrMsg(qDetail.error)}</div>
                </div>
              </div>
            </div>
          ) : !detail ? (
            <div className="text-sm text-black/60 dark:text-white/70">—</div>
          ) : (
            <div className="grid lg:grid-cols-[1fr_360px] gap-4 overflow-y-scroll pb-1 max-h-[calc(100vh-200px)]">
              {/* left: chat */}
              <div className={cn(SOFT, "p-3")}>
                <div className="block sm:flex items-start justify-between gap-2">
                  <div className="min-w-0">
                    <div className="font-extrabold truncate">{detail.subject || "-"}</div>
                    <div className="mt-1 text-[11px] text-black/55 dark:text-white/60 truncate">
                      owner: <b>{detail.owner_email || "-"}</b> (id:{toFa(detail.owner)})
                    </div>
                  </div>

                  <Badge tone={statusTone(detail.status).tone}>
                    <TbFlag className="w-4 h-4" />
                    {statusTone(detail.status).label}
                  </Badge>
                </div>

                <div className="mt-2 text-[11px] text-black/50 dark:text-white/60">
                  created: <span className="font-mono">{fmtDate(detail.created_at)}</span>
                </div>
                <div className="mt-1 text-[11px] text-black/50 dark:text-white/60">
                  updated: <span className="font-mono">{fmtDate(detail.updated_at)}</span>
                </div>

                <div className="mt-4 space-y-2 max-h-[52vh] overflow-auto pr-1">
                  {(detail.messages || []).length === 0 ? (
                    <div className="py-10 text-center text-black/50 dark:text-white/60">پیامی وجود ندارد.</div>
                  ) : (
                    detail.messages.map((m) => {
                      const isStaff = !!m?.sender_is_staff || !!m?.is_from_staff;
                      return (
                        <div
                          key={String(m.id)}
                          className={cn(
                            "rounded-2xl border p-3",
                            isStaff
                              ? "border-[#43BEFF50] bg-[#43BEFF10]"
                              : "border-black/10 dark:border-white/10 bg-white/60 dark:bg-white/5"
                          )}
                        >
                          <div className="flex items-start justify-between gap-2">
                            <div className="min-w-0">
                              <div className="text-xs font-extrabold truncate">
                                {isStaff ? "ادمین" : "کاربر"} • {m?.sender_email || "-"}
                              </div>
                              <div className="mt-1 text-[11px] text-black/50 dark:text-white/60">{fmtDate(m?.created_at)}</div>
                            </div>
                            <Badge tone={isStaff ? "info" : "default"}>{isStaff ? "ادمین" : "کاربر"}</Badge>
                          </div>

                          <div className="mt-2 text-sm text-black dark:text-white whitespace-pre-wrap texnew">{m?.body || "-"}</div>

                          {/* ✅ attachment in chat */}
                          {!!m?.attachment && <AttachmentBubble url={m.attachment} />}
                        </div>
                      );
                    })
                  )}
                </div>

                <div className="mt-3 flex items-center justify-end gap-2">
                  <button
                    type="button"
                    onClick={() => qDetail.refetch()}
                    className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition text-xs inline-flex items-center gap-2"
                  >
                    <TbRefresh className={cn("w-4 h-4", qDetail.isFetching ? "animate-spin" : "")} />
                    بروزرسانی چت
                  </button>
                </div>
              </div>

              {/* right: reply */}
              <div className={cn(SOFT, "p-3")}>
                <div className="text-sm font-extrabold inline-flex items-center gap-2">
                  <TbSend className="w-5 h-5 opacity-80" />
                  پاسخ ادمین
                </div>

                <div className="mt-3 space-y-3">
                  <Field label="new_status (اختیاری)" hint="اگر خالی باشه ارسال نمی‌شه.">
                    <DropdownPortal
                      id={`ticket-reply-status-${String(currentTicketId || "x")}`}
                      menu={ddMenu}
                      setMenu={setDdMenu}
                      label="تغییر وضعیت"
                      value={replyStatus || ""}
                      options={[{ value: "", label: "بدون تغییر" }, ...statusOptions]}
                      onChange={(v) => setReplyStatus(v)}
                      width={260}
                    />
                  </Field>

                  <Field label="متن پیام *">
                    <div className={cn(INPUT_BASE, "min-h-[160px]")}>
                      <textarea
                        className={cn(
                          "w-full min-h-[160px] p-4 outline-0 bg-transparent resize-none",
                          "text-black dark:text-white placeholder:text-black/50 dark:placeholder:text-white/50"
                        )}
                        placeholder="متن پاسخ را بنویس..."
                        value={replyBody}
                        onChange={(e) => setReplyBody(e.target.value)}
                        disabled={mutReply.isPending}
                      />
                    </div>
                    <div className="mt-1 text-[11px] text-black/50 dark:text-white/60">
                      محدودیت: فایل/صدا تا ۵MB • ضبط تا ۵ دقیقه
                    </div>
                  </Field>

                  <Field label="attachment / ضبط صدا (اختیاری)">
                    <div className={cn(SOFT, "p-3 grid gap-3")}>
                      {/* file line */}
                      <div className="grid sm:flex items-center justify-between gap-2">
                        <div className="min-w-0">
                          <div className="text-xs text-black/55 dark:text-white/60">پیوست:</div>
                          <div className="mt-1 text-sm font-bold truncate">{replyFile?.name || "—"}</div>
                          {!!replyFile && (
                            <div className="mt-1 text-[11px] text-black/50 dark:text-white/60">
                              حجم: <b>{bytesToHuman(replyFile.size)}</b>
                            </div>
                          )}
                        </div>

                        <div className="flex items-center gap-2">
                          <input
                            ref={fileRef}
                            type="file"
                            className="hidden"
                            accept="image/*,.png,.jpg,.jpeg,.webp,.pdf,audio/*,.mp3,.wav,.m4a,.webm,.ogg"
                            onChange={(e) => {
                              const f = e.target.files?.[0] || null;
                              setReplyFileWithLimit(f);
                            }}
                            disabled={mutReply.isPending || isRecording}
                          />

                          <button
                            type="button"
                            onClick={() => fileRef.current?.click()}
                            disabled={mutReply.isPending || isRecording}
                            className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition text-xs inline-flex items-center gap-2 disabled:opacity-60"
                            title={isRecording ? "اول ضبط را متوقف کن" : "انتخاب فایل"}
                          >
                            <TbPaperclip className="w-4 h-4" />
                            انتخاب فایل
                          </button>

                          {replyFile ? (
                            <button
                              type="button"
                              onClick={() => {
                                setReplyFile(null);
                                if (fileRef.current) fileRef.current.value = "";
                              }}
                              disabled={mutReply.isPending}
                              className="px-3 py-2 rounded-xl border border-red-500/25 bg-red-500/10 hover:bg-red-500/15 transition text-xs text-red-700 dark:text-red-200 inline-flex items-center gap-2 disabled:opacity-60"
                            >
                              <TbX className="w-4 h-4" />
                              حذف
                            </button>
                          ) : null}
                        </div>
                      </div>

                      {/* recorder line */}
                      <div className="grid sm:flex items-center justify-between gap-2 flex-wrap">
                        <div className="text-xs text-black/55 dark:text-white/60">
                          ضبط صدا: {isRecording ? <b className="text-red-600 dark:text-red-300">{formatTime(recordSec)} / {formatTime(MAX_RECORD_SECONDS)}</b> : "—"}
                        </div>

                        <div className="flex items-center gap-2">
                          <button
                            type="button"
                            onClick={startRecording}
                            disabled={mutReply.isPending || isRecording || !canRecord}
                            className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition text-xs inline-flex items-center gap-2 disabled:opacity-60"
                            title={!canRecord ? "مرورگر ضبط صدا را پشتیبانی نمی‌کند" : "شروع ضبط"}
                          >
                            <TbMicrophone className="w-4 h-4" />
                            ضبط
                          </button>

                          <button
                            type="button"
                            onClick={stopRecording}
                            disabled={mutReply.isPending || !isRecording}
                            className="px-3 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-red-500/10 transition text-xs inline-flex items-center gap-2 disabled:opacity-60"
                            title="توقف"
                          >
                            <TbPlayerStop className="w-4 h-4 text-red-600 dark:text-red-300" />
                            توقف
                          </button>

                          <button
                            type="button"
                            onClick={() => {
                              discardRecording();
                              setReplyFile(null);
                              if (fileRef.current) fileRef.current.value = "";
                              info("پیوست/ضبط حذف شد.", { title: "Reply" });
                            }}
                            disabled={mutReply.isPending || (!replyFile && !isRecording)}
                            className="px-3 py-2 rounded-xl border border-red-500/25 bg-red-500/10 hover:bg-red-500/15 transition text-xs text-red-700 dark:text-red-200 inline-flex items-center gap-2 disabled:opacity-60"
                            title="حذف"
                          >
                            <TbTrash className="w-4 h-4" />
                            پاک کردن
                          </button>
                        </div>
                      </div>
                    </div>
                  </Field>

                  <div className="flex flex-col sm:flex-row gap-2 sm:justify-end">
                    <button
                      type="button"
                      disabled={mutReply.isPending}
                      onClick={closeDetails}
                      className="px-4 py-2 rounded-xl border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition disabled:opacity-60"
                    >
                      بستن
                    </button>

                    <button
                      type="button"
                      disabled={mutReply.isPending || !currentTicketId}
                      onClick={submitReply}
                      className="px-4 py-2 rounded-xl shadow border border-black/10 dark:border-white/10 bg-white dark:bg-[#16325b]
                             hover:bg-black/5 dark:hover:bg-white/10 transition inline-flex items-center justify-center gap-2 disabled:opacity-60"
                    >
                      {mutReply.isPending ? (
                        <>
                          <TbRefresh className="w-4 h-4 animate-spin text-[#43BEFF] dark:text-white" />
                          درحال ارسال...
                        </>
                      ) : (
                        <>
                          <TbCheck className="w-4 h-4 text-[#43BEFF] dark:text-white" />
                          ارسال پاسخ
                        </>
                      )}
                    </button>
                  </div>

                  {replyFile && replyFile.size > MAX_UPLOAD_BYTES ? (
                    <div className="text-xs text-red-700 dark:text-red-200">
                      حجم فایل بیشتر از ۵MB است.
                    </div>
                  ) : null}
                </div>
              </div>
            </div>
          )}
        </Modal>
      </div>
    </PageShell>
  );
}
