"use client";

import { useEffect, useRef, useState } from "react";
import { ArrowUp, AtSign, FileText, Loader2, Lock, MessageCircle, Mic, MicOff, Paperclip, Trash2, X } from "lucide-react";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/presentation/components/ui/tooltip";
import { useApiQuery } from "@/presentation/hooks/use-api-query";
import { useDebounce } from "@/presentation/hooks/use-debounce";
import { useRepositories } from "@/di/repository-provider";
import { useAuth } from "@/presentation/hooks/use-auth";
import { useT } from "@/shared/i18n/client";
import { cn } from "@/shared/lib/utils";
import { displayName } from "@/shared/lib/user-display";
import { EmojiPicker } from "./emoji-picker";
import { PersonAvatar } from "./person-avatar";
import { formatDuration } from "./support-utils";
import { isVoiceRecordingSupported, microphoneIssueOf, useVoiceRecorder, type MicrophoneIssue } from "./use-voice-recorder";
import type { CreateTicketMessagePayload, TicketAssignableUser, TicketMessageKind } from "@/domain/entities/ticket";

const MAX_FILE_SIZE = 10 * 1024 * 1024;
const MAX_FILES = 10;
const DRAFT_DEBOUNCE_MS = 800;

// Backend parses `@First.Last`, so multi-word names are joined with a dot, without accents.
function toMentionHandle(name: string): string {
  return name.trim().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^\w]+/g, ".").replace(/^\.+|\.+$/g, "");
}

interface SupportComposerProps {
  ticketId: number;
  /** The client confirmed the resolution: only internal notes can be added. */
  repliesClosed?: boolean;
  /** Resolves false when the send failed, so the text and files are kept. */
  onSend: (payload: CreateTicketMessagePayload) => Promise<boolean>;
}

export function SupportComposer({ ticketId, repliesClosed = false, onSend }: SupportComposerProps) {
  const t = useT();
  const container = useRepositories();
  const { user } = useAuth();
  const [kind, setKind] = useState<TicketMessageKind>(repliesClosed ? "internal" : "client");
  const [body, setBody] = useState("");
  const [files, setFiles] = useState<File[]>([]);
  const [sending, setSending] = useState(false);
  const [sendingVoice, setSendingVoice] = useState(false);
  const [dragOver, setDragOver] = useState(false);
  const [draftLoaded, setDraftLoaded] = useState(false);
  const [micIssue, setMicIssue] = useState<MicrophoneIssue | null>(null);
  const [mentionQuery, setMentionQuery] = useState<string | null>(null);
  const [mentions, setMentions] = useState<{ id: number; handle: string }[]>([]);
  const textarea = useRef<HTMLTextAreaElement>(null);
  const fileInput = useRef<HTMLInputElement>(null);
  const isNote = kind === "internal";

  // Drafts are per conversation and per kind, saved server-side.
  useEffect(() => {
    let cancelled = false;
    setDraftLoaded(false);
    container.ticketRepository
      .getDraft(ticketId, kind)
      .then((draft) => { if (!cancelled) setBody(draft?.body ?? ""); })
      .catch(() => {})
      .finally(() => { if (!cancelled) { setDraftLoaded(true); setMentions([]); } });
    return () => { cancelled = true; };
  }, [ticketId, kind, container]);

  const debouncedBody = useDebounce(body, DRAFT_DEBOUNCE_MS);
  useEffect(() => {
    if (!draftLoaded) return;
    const save = debouncedBody.trim()
      ? container.ticketRepository.upsertDraft(ticketId, kind, debouncedBody)
      : container.ticketRepository.deleteDraft(ticketId, kind);
    void save.catch(() => {});
  }, [debouncedBody, draftLoaded, ticketId, kind, container]);

  // Grow with the text, up to a limit.
  useEffect(() => {
    const el = textarea.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = `${Math.min(el.scrollHeight, 180)}px`;
  }, [body, kind]);

  useEffect(() => { textarea.current?.focus(); }, [ticketId, kind]);

  const debouncedMention = useDebounce(mentionQuery ?? "", 150);
  const { data: mentionable } = useApiQuery(
    () => container.ticketRepository.getMentionableUsers(debouncedMention || undefined),
    { deps: [debouncedMention], enabled: isNote && mentionQuery !== null },
  );
  const mentionOptions = (mentionable ?? []).filter((u) => u.id !== user?.id).slice(0, 6);

  const sendVoice = async () => {
    if (sendingVoice) return;
    setSendingVoice(true);
    try {
      const voice = await recorder.stop();
      if (!voice) {
        toast.error(t.support.composer.emptyVoice);
        return;
      }
      await onSend({ kind, attachments: [voice] });
    } finally {
      setSendingVoice(false);
    }
  };
  const recorder = useVoiceRecorder(sendVoice);

  const startRecording = async () => {
    setMicIssue(null);
    try {
      await recorder.start();
    } catch (error) {
      setMicIssue(microphoneIssueOf(error));
    }
  };

  function addFiles(incoming: File[]) {
    const accepted = incoming.filter((file) => {
      if (file.size > MAX_FILE_SIZE) {
        toast.error(t.support.composer.fileTooLarge(file.name));
        return false;
      }
      return true;
    });
    if (files.length + accepted.length > MAX_FILES) toast.error(t.ticketDetail.communications.tooManyFiles);
    setFiles((prev) => [...prev, ...accepted].slice(0, MAX_FILES));
  }

  function onBodyChange(value: string) {
    setBody(value);
    if (!isNote) return;
    const caret = textarea.current?.selectionStart ?? value.length;
    const match = value.slice(0, caret).match(/@([\w.]*)$/);
    setMentionQuery(match ? match[1] : null);
  }

  function pickMention(member: TicketAssignableUser) {
    const el = textarea.current;
    if (!el) return;
    const caret = el.selectionStart;
    const handle = toMentionHandle(displayName(member));
    const before = body.slice(0, caret).replace(/@([\w.]*)$/, `@${handle} `);
    setBody(before + body.slice(caret));
    setMentions((prev) => (prev.some((m) => m.id === member.id) ? prev : [...prev, { id: member.id, handle }]));
    setMentionQuery(null);
    requestAnimationFrame(() => {
      el.focus();
      el.setSelectionRange(before.length, before.length);
    });
  }

  function insertEmoji(emoji: string) {
    const el = textarea.current;
    const start = el?.selectionStart ?? body.length;
    const end = el?.selectionEnd ?? body.length;
    setBody(body.slice(0, start) + emoji + body.slice(end));
    requestAnimationFrame(() => {
      el?.focus();
      el?.setSelectionRange(start + emoji.length, start + emoji.length);
    });
  }

  const canSend = !sending && (body.trim() !== "" || files.length > 0);

  async function send() {
    if (!canSend) return;
    setSending(true);
    const mentionIds = Array.from(new Set(mentions.filter((m) => body.includes(`@${m.handle}`)).map((m) => m.id)));
    const ok = await onSend({
      kind,
      body: body.trim() || undefined,
      attachments: files,
      mentions: isNote && mentionIds.length ? mentionIds : undefined,
    });
    setSending(false);
    if (!ok) return;
    setBody("");
    setFiles([]);
    setMentions([]);
    textarea.current?.focus();
  }

  const issueText = micIssue === "no-device"
    ? t.support.composer.micMissing
    : micIssue === "busy"
      ? t.support.composer.micBusy
      : t.support.composer.micBlocked;

  return (
    <div className="border-t border-border bg-card px-4 pb-4 pt-3">
      <div className="mb-2 flex items-center gap-1">
        {(repliesClosed ? (["internal"] as const) : (["client", "internal"] as const)).map((value) => (
          <button
            key={value}
            type="button"
            onClick={() => setKind(value)}
            className={cn(
              "inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors",
              kind === value
                ? value === "internal"
                  ? "bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300"
                  : "bg-primary/10 text-primary"
                : "text-muted-foreground hover:bg-muted hover:text-foreground",
            )}
          >
            {value === "internal" ? <Lock className="h-3 w-3" /> : <MessageCircle className="h-3 w-3" />}
            {value === "internal" ? t.support.composer.note : t.support.composer.reply}
          </button>
        ))}
        {isNote && <span className="ml-2 text-[11px] text-amber-700 dark:text-amber-400">{t.support.composer.noteHint}</span>}
      </div>

      {micIssue && (
        <div role="alert" className="mb-2 flex items-start gap-2 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-300">
          <MicOff className="mt-0.5 h-3.5 w-3.5 shrink-0" />
          <span className="flex-1">{issueText}</span>
          <button type="button" onClick={startRecording} className="shrink-0 font-semibold underline-offset-2 hover:underline">
            {t.support.composer.retry}
          </button>
          <button type="button" onClick={() => setMicIssue(null)} className="shrink-0 opacity-70 hover:opacity-100">
            <X className="h-3.5 w-3.5" />
          </button>
        </div>
      )}

      <div
        onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={(e) => { e.preventDefault(); setDragOver(false); addFiles(Array.from(e.dataTransfer.files)); }}
        className={cn(
          "relative rounded-2xl border bg-background transition-all",
          isNote
            ? "border-amber-300 bg-amber-50/50 focus-within:ring-4 focus-within:ring-amber-200/60 dark:border-amber-800 dark:bg-amber-950/10"
            : "border-border focus-within:border-primary/40 focus-within:ring-4 focus-within:ring-primary/10",
          dragOver && "ring-4 ring-primary/20",
        )}
      >
        {sendingVoice ? (
          <div className="flex h-[88px] items-center gap-2.5 px-4 text-sm text-muted-foreground">
            <Loader2 className="h-4 w-4 animate-spin" /> {t.support.composer.sendingVoice}
          </div>
        ) : recorder.isRecording ? (
          <div className="flex h-[88px] items-center gap-3 px-4">
            <span className="relative flex h-3 w-3">
              <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
              <span className="relative inline-flex h-3 w-3 rounded-full bg-red-500" />
            </span>
            <span className="text-sm font-medium tabular-nums text-foreground">{formatDuration(recorder.elapsed)}</span>
            <span aria-label={t.support.composer.recording} className="flex h-7 flex-1 items-center justify-end gap-[2px] overflow-hidden">
              {Array.from({ length: 40 }, (_, index) => recorder.levels[recorder.levels.length - 40 + index] ?? 0).map((level, index) => (
                <span
                  key={index}
                  className="w-[3px] shrink-0 rounded-full bg-primary transition-[height] duration-100"
                  style={{ height: `${Math.max(10, level * 100)}%`, opacity: 0.35 + level * 0.65 }}
                />
              ))}
            </span>
            <IconAction label={t.support.composer.cancel} onClick={recorder.cancel}>
              <Trash2 className="h-4 w-4" />
            </IconAction>
            <button
              type="button"
              onClick={sendVoice}
              aria-label={t.support.composer.send}
              className="flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-transform hover:scale-105"
            >
              <ArrowUp className="h-4 w-4" />
            </button>
          </div>
        ) : (
          <>
            {files.length > 0 && (
              <div className="flex flex-wrap gap-2 px-3 pt-3">
                {files.map((file, index) => (
                  <FileChip key={`${file.name}-${index}`} file={file} onRemove={() => setFiles((prev) => prev.filter((_, i) => i !== index))} />
                ))}
              </div>
            )}

            <textarea
              ref={textarea}
              value={body}
              rows={2}
              onChange={(e) => onBodyChange(e.target.value)}
              onPaste={(e) => {
                const pasted = Array.from(e.clipboardData.files);
                if (pasted.length) { e.preventDefault(); addFiles(pasted); }
              }}
              onKeyDown={(e) => {
                if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && mentionQuery === null) {
                  e.preventDefault();
                  void send();
                }
                if (e.key === "Escape") setMentionQuery(null);
              }}
              placeholder={isNote ? t.support.composer.notePlaceholder : t.support.composer.replyPlaceholder}
              className="block max-h-[180px] w-full resize-none bg-transparent px-4 pt-3 text-sm leading-relaxed outline-none placeholder:text-muted-foreground"
            />

            {isNote && mentionQuery !== null && mentionOptions.length > 0 && (
              <div className="absolute bottom-full left-3 z-20 mb-2 w-64 overflow-hidden rounded-xl border border-border bg-popover shadow-lg">
                <div className="flex items-center gap-1 border-b border-border px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground">
                  <AtSign className="h-3 w-3" /> {t.support.composer.mentionHeader}
                </div>
                {mentionOptions.map((member) => (
                  <button
                    key={member.id}
                    type="button"
                    onMouseDown={(e) => { e.preventDefault(); pickMention(member); }}
                    className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted"
                  >
                    <PersonAvatar name={member.name} email={member.email} initials={member.initials} size={22} />
                    {displayName(member)}
                  </button>
                ))}
              </div>
            )}

            <div className="flex items-center gap-0.5 px-2 pb-2">
              <IconAction label={t.support.composer.attach} onClick={() => fileInput.current?.click()} disabled={files.length >= MAX_FILES}>
                <Paperclip className="h-4 w-4" />
              </IconAction>
              <EmojiPicker label={t.support.composer.emoji} onSelect={insertEmoji} />
              {isVoiceRecordingSupported() && (
                <IconAction label={t.support.composer.record} onClick={startRecording}>
                  <Mic className="h-4 w-4" />
                </IconAction>
              )}
              <span className="ml-2 hidden text-[11px] text-muted-foreground xl:inline">{t.support.composer.enterHint}</span>
              <button
                type="button"
                onClick={() => void send()}
                disabled={!canSend}
                className={cn(
                  "ml-auto inline-flex h-9 items-center gap-1.5 rounded-full px-4 text-sm font-medium shadow-sm transition-all disabled:cursor-not-allowed disabled:opacity-40",
                  isNote ? "bg-amber-500 text-white hover:bg-amber-600" : "bg-primary text-primary-foreground hover:bg-primary/90",
                )}
              >
                {sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-4 w-4" />}
                {isNote ? t.support.composer.addNote : t.support.composer.send}
              </button>
            </div>
          </>
        )}
      </div>

      <input
        ref={fileInput}
        type="file"
        multiple
        className="hidden"
        onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }}
      />
    </div>
  );
}

function IconAction({ label, onClick, disabled, children }: { label: string; onClick: () => void; disabled?: boolean; children: React.ReactNode }) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <button
          type="button"
          onClick={onClick}
          disabled={disabled}
          aria-label={label}
          className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
        >
          {children}
        </button>
      </TooltipTrigger>
      <TooltipContent>{label}</TooltipContent>
    </Tooltip>
  );
}

function FileChip({ file, onRemove }: { file: File; onRemove: () => void }) {
  const [preview] = useState(() => (file.type.startsWith("image/") ? URL.createObjectURL(file) : null));
  useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]);

  return (
    <span className="group relative">
      {preview ? (
        // eslint-disable-next-line @next/next/no-img-element
        <img src={preview} alt={file.name} className="h-14 w-14 rounded-lg object-cover ring-1 ring-border" />
      ) : (
        <span className="flex h-14 max-w-44 items-center gap-2 rounded-lg bg-muted px-3 text-xs">
          <FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
          <span className="truncate">{file.name}</span>
        </span>
      )}
      <button
        type="button"
        onClick={onRemove}
        className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-foreground text-background shadow"
      >
        <X className="h-3 w-3" />
      </button>
    </span>
  );
}
