import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from "react";
import { attachFile, requestUploadUrl, sendMessageWithClientId, uploadToPresignedUrl } from "../api/chats";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
import { buildWsUrl } from "../utils/ws";

type RecordingState = "idle" | "recording" | "locked";

export function MessageComposer() {
  const activeChatId = useChatStore((s) => s.activeChatId);
  const me = useAuthStore((s) => s.me);
  const draftsByChat = useChatStore((s) => s.draftsByChat);
  const setDraft = useChatStore((s) => s.setDraft);
  const clearDraft = useChatStore((s) => s.clearDraft);
  const addOptimisticMessage = useChatStore((s) => s.addOptimisticMessage);
  const confirmMessageByClientId = useChatStore((s) => s.confirmMessageByClientId);
  const removeOptimisticMessage = useChatStore((s) => s.removeOptimisticMessage);
  const replyToByChat = useChatStore((s) => s.replyToByChat);
  const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
  const accessToken = useAuthStore((s) => s.accessToken);

  const [text, setText] = useState("");
  const wsRef = useRef<WebSocket | null>(null);
  const recorderRef = useRef<MediaRecorder | null>(null);
  const recordingStreamRef = useRef<MediaStream | null>(null);
  const chunksRef = useRef<BlobPart[]>([]);
  const sendVoiceOnStopRef = useRef<boolean>(true);
  const recordingStartedAtRef = useRef<number | null>(null);
  const pointerStartYRef = useRef<number>(0);
  const pointerStartXRef = useRef<number>(0);
  const pointerCancelArmedRef = useRef<boolean>(false);
  const pointerMoveHandlerRef = useRef<((event: globalThis.PointerEvent) => void) | null>(null);
  const pointerUpHandlerRef = useRef<((event: globalThis.PointerEvent) => void) | null>(null);
  const recordingStateRef = useRef<RecordingState>("idle");

  const [isUploading, setIsUploading] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [selectedType, setSelectedType] = useState<"file" | "image" | "video" | "audio">("file");
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [showAttachMenu, setShowAttachMenu] = useState(false);
  const [captionDraft, setCaptionDraft] = useState("");
  const mediaInputRef = useRef<HTMLInputElement | null>(null);
  const fileInputRef = useRef<HTMLInputElement | null>(null);

  const [recordingState, setRecordingState] = useState<RecordingState>("idle");
  const [recordSeconds, setRecordSeconds] = useState(0);
  const [dragHint, setDragHint] = useState<"idle" | "lock" | "cancel">("idle");

  useEffect(() => {
    recordingStateRef.current = recordingState;
  }, [recordingState]);

  useEffect(() => {
    if (!activeChatId) {
      if (text !== "") {
        setText("");
      }
      return;
    }
    const draft = draftsByChat[activeChatId] ?? "";
    if (draft !== text) {
      setText(draft);
    }
  }, [activeChatId, draftsByChat, text]);

  useEffect(() => {
    return () => {
      if (previewUrl) {
        URL.revokeObjectURL(previewUrl);
      }
      if (pointerMoveHandlerRef.current) {
        window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
      }
      if (pointerUpHandlerRef.current) {
        window.removeEventListener("pointerup", pointerUpHandlerRef.current);
      }
    };
  }, [previewUrl]);

  useEffect(() => {
    if (recordingState === "idle") {
      return;
    }
    const interval = window.setInterval(() => {
      if (!recordingStartedAtRef.current) {
        return;
      }
      const sec = Math.max(0, Math.floor((Date.now() - recordingStartedAtRef.current) / 1000));
      setRecordSeconds(sec);
    }, 250);
    return () => window.clearInterval(interval);
  }, [recordingState]);

  function makeClientMessageId(): string {
    if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
      return crypto.randomUUID();
    }
    return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
  }

  function getWs(): WebSocket | null {
    if (!accessToken || !activeChatId) {
      return null;
    }
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      return wsRef.current;
    }
    const wsUrl = buildWsUrl(accessToken);
    wsRef.current = new WebSocket(wsUrl);
    return wsRef.current;
  }

  async function handleSend() {
    if (!activeChatId || !text.trim() || !me) {
      return;
    }
    const clientMessageId = makeClientMessageId();
    const textValue = text.trim();
    const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined;
    addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: "text", text: textValue, clientMessageId });
    try {
      let message = null;
      for (let attempt = 0; attempt < 2; attempt += 1) {
        try {
          message = await sendMessageWithClientId(activeChatId, textValue, "text", clientMessageId, replyToMessageId);
          break;
        } catch (error) {
          if (attempt === 1) {
            throw error;
          }
          await new Promise((resolve) => window.setTimeout(resolve, 250));
        }
      }
      if (!message) {
        throw new Error("send failed");
      }
      confirmMessageByClientId(activeChatId, clientMessageId, message);
      setText("");
      clearDraft(activeChatId);
      setReplyToMessage(activeChatId, null);
      const ws = getWs();
      ws?.send(JSON.stringify({ event: "typing_stop", payload: { chat_id: activeChatId } }));
    } catch {
      removeOptimisticMessage(activeChatId, clientMessageId);
      setUploadError("Message send failed. Please try again.");
    }
  }

  function onComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      void handleSend();
    }
  }

  async function handleUpload(file: File, messageType: "file" | "image" | "video" | "audio" | "voice" = "file") {
    if (!activeChatId || !me) {
      return;
    }
    setIsUploading(true);
    setUploadProgress(0);
    setUploadError(null);
    const clientMessageId = makeClientMessageId();
    const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined;
    try {
      const upload = await requestUploadUrl(file);
      await uploadToPresignedUrl(upload.upload_url, upload.required_headers, file, setUploadProgress);
      addOptimisticMessage({
        chatId: activeChatId,
        senderId: me.id,
        type: messageType,
        text: upload.file_url,
        clientMessageId
      });
      const message = await sendMessageWithClientId(activeChatId, upload.file_url, messageType, clientMessageId, replyToMessageId);
      confirmMessageByClientId(activeChatId, clientMessageId, message);
      try {
        await attachFile(message.id, upload.file_url, file.type || "application/octet-stream", file.size);
      } catch {
        setUploadError("File sent, but metadata save failed. Please refresh chat.");
      }
      setReplyToMessage(activeChatId, null);
    } catch {
      removeOptimisticMessage(activeChatId, clientMessageId);
      setUploadError("Upload failed. Please try again.");
    } finally {
      setIsUploading(false);
    }
  }

  function inferType(file: File): "file" | "image" | "video" | "audio" {
    if (file.type.startsWith("image/")) return "image";
    if (file.type.startsWith("video/")) return "video";
    if (file.type.startsWith("audio/")) return "audio";
    return "file";
  }

  function loadImageFromFile(file: File): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const image = new Image();
      const url = URL.createObjectURL(file);
      image.onload = () => {
        URL.revokeObjectURL(url);
        resolve(image);
      };
      image.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error("Failed to load image"));
      };
      image.src = url;
    });
  }

  async function compressImageForWeb(file: File): Promise<File> {
    const image = await loadImageFromFile(file);
    const maxSide = 1920;
    const ratio = Math.min(1, maxSide / Math.max(image.width, image.height));
    const width = Math.max(1, Math.round(image.width * ratio));
    const height = Math.max(1, Math.round(image.height * ratio));

    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      return file;
    }
    ctx.drawImage(image, 0, 0, width, height);

    const blob = await new Promise<Blob | null>((resolve) => {
      canvas.toBlob((result) => resolve(result), "image/jpeg", 0.82);
    });
    if (!blob || blob.size >= file.size) {
      return file;
    }
    const baseName = file.name.replace(/\.[^/.]+$/, "");
    return new File([blob], `${baseName || "image"}-web.jpg`, { type: "image/jpeg" });
  }

  async function prepareFileForUpload(file: File, fileType: "file" | "image" | "video" | "audio"): Promise<File> {
    if (fileType === "image") {
      return compressImageForWeb(file);
    }
    return file;
  }

  async function startRecord() {
    if (recordingState !== "idle") {
      return false;
    }
    try {
      if (navigator.permissions && navigator.permissions.query) {
        const permission = await navigator.permissions.query({ name: "microphone" as PermissionName });
        if (permission.state === "denied") {
          setUploadError("Microphone access denied. Allow microphone in browser site permissions.");
          return false;
        }
      }
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const recorder = new MediaRecorder(stream);
      recordingStreamRef.current = stream;
      chunksRef.current = [];
      sendVoiceOnStopRef.current = true;
      recorder.ondataavailable = (event) => chunksRef.current.push(event.data);
      recorder.onstop = async () => {
        const shouldSend = sendVoiceOnStopRef.current;
        const data = [...chunksRef.current];
        chunksRef.current = [];
        if (recordingStreamRef.current) {
          recordingStreamRef.current.getTracks().forEach((track) => track.stop());
          recordingStreamRef.current = null;
        }
        if (!shouldSend || data.length === 0) {
          return;
        }
        const blob = new Blob(data, { type: "audio/webm" });
        const file = new File([blob], `voice-${Date.now()}.webm`, { type: "audio/webm" });
        await handleUpload(file, "voice");
      };
      recorderRef.current = recorder;
      recorder.start();
      recordingStartedAtRef.current = Date.now();
      setRecordSeconds(0);
      setRecordingState("recording");
      return true;
    } catch {
      setUploadError("Microphone access denied. Please allow microphone and retry.");
      return false;
    }
  }

  function stopRecord(send: boolean) {
    sendVoiceOnStopRef.current = send;
    pointerCancelArmedRef.current = false;
    setDragHint("idle");
    if (recorderRef.current && recorderRef.current.state !== "inactive") {
      recorderRef.current.stop();
    }
    recorderRef.current = null;
    recordingStartedAtRef.current = null;
    setRecordingState("idle");
    setRecordSeconds(0);
  }

  async function onMicPointerDown(event: PointerEvent<HTMLButtonElement>) {
    event.preventDefault();
    const started = await startRecord();
    if (!started) {
      return;
    }
    pointerStartYRef.current = event.clientY;
    pointerStartXRef.current = event.clientX;
    pointerCancelArmedRef.current = false;
    setDragHint("idle");

    if (pointerMoveHandlerRef.current) {
      window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
    }
    if (pointerUpHandlerRef.current) {
      window.removeEventListener("pointerup", pointerUpHandlerRef.current);
    }

    const onPointerMove = (moveEvent: globalThis.PointerEvent) => {
      const current = recordingStateRef.current;
      if (current === "idle") {
        return;
      }
      const deltaY = pointerStartYRef.current - moveEvent.clientY;
      const deltaX = pointerStartXRef.current - moveEvent.clientX;

      if (current === "recording" && deltaY > 70) {
        setRecordingState("locked");
        setDragHint("idle");
        return;
      }

      if (current === "recording") {
        if (deltaX > 90) {
          pointerCancelArmedRef.current = true;
          setDragHint("cancel");
        } else if (deltaY > 40) {
          pointerCancelArmedRef.current = false;
          setDragHint("lock");
        } else {
          pointerCancelArmedRef.current = false;
          setDragHint("idle");
        }
      }
    };

    const onPointerUp = () => {
      if (pointerMoveHandlerRef.current) {
        window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
      }
      if (pointerUpHandlerRef.current) {
        window.removeEventListener("pointerup", pointerUpHandlerRef.current);
      }
      pointerMoveHandlerRef.current = null;
      pointerUpHandlerRef.current = null;

      const current = recordingStateRef.current;
      if (current === "recording") {
        stopRecord(!pointerCancelArmedRef.current);
      }
      setDragHint("idle");
    };

    pointerMoveHandlerRef.current = onPointerMove;
    pointerUpHandlerRef.current = onPointerUp;
    window.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerup", onPointerUp);
  }

  function selectFile(file: File) {
    setUploadError(null);
    setSelectedFile(file);
    setShowAttachMenu(false);
    const fileType = inferType(file);
    setSelectedType(fileType);
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    if (fileType === "image" || fileType === "video") {
      setPreviewUrl(URL.createObjectURL(file));
    } else {
      setPreviewUrl(null);
    }
  }

  async function sendSelectedFile() {
    if (!selectedFile) {
      return;
    }
    const uploadFile = await prepareFileForUpload(selectedFile, selectedType);
    await handleUpload(uploadFile, selectedType);
    if (captionDraft.trim() && activeChatId && me) {
      const clientMessageId = makeClientMessageId();
      const textValue = captionDraft.trim();
      addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: "text", text: textValue, clientMessageId });
      try {
        const message = await sendMessageWithClientId(activeChatId, textValue, "text", clientMessageId);
        confirmMessageByClientId(activeChatId, clientMessageId, message);
      } catch {
        removeOptimisticMessage(activeChatId, clientMessageId);
      }
    }
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    setSelectedFile(null);
    setPreviewUrl(null);
    setCaptionDraft("");
    setSelectedType("file");
    setUploadProgress(0);
  }

  function cancelSelectedFile() {
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    setSelectedFile(null);
    setPreviewUrl(null);
    setCaptionDraft("");
    setSelectedType("file");
    setUploadProgress(0);
    setUploadError(null);
  }

  function formatBytes(size: number): string {
    if (size < 1024) return `${size} B`;
    if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
    return `${(size / (1024 * 1024)).toFixed(1)} MB`;
  }

  return (
    <div className="border-t border-slate-700/50 bg-slate-900/55 p-3">
      {activeChatId && replyToByChat[activeChatId] ? (
        <div className="mb-2 flex items-start justify-between rounded-lg border border-slate-700/80 bg-slate-800/70 px-3 py-2 text-xs">
          <div className="min-w-0">
            <p className="font-semibold text-sky-300">Replying</p>
            <p className="truncate text-slate-300">{replyToByChat[activeChatId]?.text || "[media]"}</p>
          </div>
          <button className="ml-2 rounded bg-slate-700 px-2 py-1 text-[11px]" onClick={() => setReplyToMessage(activeChatId, null)}>
            Cancel
          </button>
        </div>
      ) : null}

      {recordingState !== "idle" ? (
        <div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 px-3 py-2 text-xs text-slate-200">
          <div className="flex items-center justify-between">
            <span>🎤 Recording {formatDuration(recordSeconds)}</span>
            {recordingState === "recording" ? (
              <span className={dragHint === "cancel" ? "text-red-300" : dragHint === "lock" ? "text-sky-300" : "text-slate-400"}>
                {dragHint === "cancel" ? "Release to cancel" : dragHint === "lock" ? "Release up to lock" : "Slide up to lock, left to cancel"}
              </span>
            ) : (
              <span className="text-sky-300">Locked</span>
            )}
          </div>
          {recordingState === "locked" ? (
            <div className="mt-2 flex gap-2">
              <button className="w-full rounded bg-slate-700 px-3 py-1.5" onClick={() => stopRecord(false)} type="button">
                Cancel
              </button>
              <button className="w-full rounded bg-sky-500 px-3 py-1.5 font-semibold text-slate-950" onClick={() => stopRecord(true)} type="button">
                Send Voice
              </button>
            </div>
          ) : null}
        </div>
      ) : null}

      <div className="mb-2 flex items-end gap-2">
        <div className="relative">
          <button
            className="h-[42px] min-w-[42px] rounded-full bg-slate-700/85 px-3 text-sm font-semibold text-slate-200 hover:bg-slate-700 disabled:opacity-60"
            disabled={isUploading || recordingState !== "idle"}
            onClick={() => setShowAttachMenu((v) => !v)}
            type="button"
          >
            📎
          </button>
          {showAttachMenu ? (
            <div className="absolute bottom-12 left-0 z-20 w-44 rounded-xl border border-slate-700/80 bg-slate-900/95 p-1.5 shadow-2xl">
              <button
                className="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800"
                onClick={() => mediaInputRef.current?.click()}
                type="button"
              >
                Photo or Video
              </button>
              <button
                className="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800"
                onClick={() => fileInputRef.current?.click()}
                type="button"
              >
                File
              </button>
            </div>
          ) : null}
          <input
            ref={mediaInputRef}
            className="hidden"
            type="file"
            accept="image/*,video/*"
            disabled={isUploading || recordingState !== "idle"}
            onChange={(event) => {
              const file = event.target.files?.[0];
              if (file) {
                selectFile(file);
              }
              event.currentTarget.value = "";
            }}
          />
          <input
            ref={fileInputRef}
            className="hidden"
            type="file"
            disabled={isUploading || recordingState !== "idle"}
            onChange={(event) => {
              const file = event.target.files?.[0];
              if (file) {
                selectFile(file);
              }
              event.currentTarget.value = "";
            }}
          />
        </div>

        <textarea
          className="min-h-[42px] max-h-40 flex-1 resize-y rounded-2xl border border-slate-700/80 bg-slate-800/80 px-4 py-2.5 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
          placeholder="Write a message..."
          rows={1}
          value={text}
          onKeyDown={onComposerKeyDown}
          onChange={(event) => {
            const next = event.target.value;
            setText(next);
            if (activeChatId) {
              setDraft(activeChatId, next);
              const ws = getWs();
              ws?.send(JSON.stringify({ event: "typing_start", payload: { chat_id: activeChatId } }));
            }
          }}
        />

        <button
          className="h-[42px] w-[72px] rounded-full bg-sky-500 px-4 text-sm font-semibold text-slate-950 hover:bg-sky-400 disabled:opacity-60"
          disabled={recordingState !== "idle"}
          onClick={handleSend}
          type="button"
        >
          Send
        </button>

        <button
          className={`h-[42px] w-[56px] rounded-full px-3 text-sm font-semibold ${recordingState === "idle" ? "bg-emerald-500 text-slate-950 hover:bg-emerald-400" : "bg-slate-700 text-slate-300"}`}
          disabled={isUploading}
          onPointerDown={onMicPointerDown}
          type="button"
        >
          Mic
        </button>
      </div>

      {selectedFile ? (
        <div className="fixed inset-0 z-[160] flex items-center justify-center bg-slate-950/80 p-3" onClick={cancelSelectedFile}>
          <div className="flex h-full w-full max-w-2xl flex-col rounded-2xl border border-slate-700/80 bg-slate-900/95 shadow-2xl" onClick={(event) => event.stopPropagation()}>
            <div className="flex items-center justify-between border-b border-slate-700/70 px-3 py-2">
              <div className="min-w-0">
                <p className="truncate text-sm font-semibold">Send {selectedType === "image" || selectedType === "video" ? "Photo" : "File"}</p>
                <p className="truncate text-xs text-slate-400">{selectedFile.name} • {formatBytes(selectedFile.size)}</p>
              </div>
              <button className="rounded bg-slate-700 px-2 py-1 text-xs" onClick={cancelSelectedFile} type="button">
                Close
              </button>
            </div>

            <div className="tg-scrollbar min-h-0 flex-1 overflow-auto p-3">
              {previewUrl && selectedType === "image" ? (
                <img className="mx-auto max-h-[70vh] w-full rounded-xl object-contain" src={previewUrl} alt={selectedFile.name} />
              ) : null}
              {previewUrl && selectedType === "video" ? (
                <video className="mx-auto max-h-[70vh] w-full rounded-xl" src={previewUrl} controls muted />
              ) : null}
              {!previewUrl ? (
                <div className="rounded-lg border border-slate-700/80 bg-slate-800/60 px-3 py-2 text-xs text-slate-300">
                  No preview available
                </div>
              ) : null}
            </div>

            <div className="border-t border-slate-700/70 p-3">
              <input
                className="mb-2 w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
                maxLength={1000}
                placeholder="Add a caption..."
                value={captionDraft}
                onChange={(event) => setCaptionDraft(event.target.value)}
              />

              {isUploading ? (
                <div className="mb-2">
                  <div className="mb-1 text-xs text-slate-300">Uploading: {uploadProgress}%</div>
                  <div className="h-2 rounded bg-slate-700">
                    <div className="h-2 rounded bg-sky-500 transition-all" style={{ width: `${uploadProgress}%` }} />
                  </div>
                </div>
              ) : null}
              <div className="flex gap-2">
                <button
                  className="w-full rounded-xl bg-sky-500 px-3 py-2 font-semibold text-slate-950 disabled:opacity-50"
                  onClick={() => void sendSelectedFile()}
                  disabled={isUploading}
                >
                  Send
                </button>
                <button className="w-full rounded-xl bg-slate-700 px-3 py-2 disabled:opacity-50" onClick={cancelSelectedFile} disabled={isUploading}>
                  Cancel
                </button>
              </div>
            </div>
          </div>
        </div>
      ) : null}

      {uploadError ? <div className="mb-2 text-sm text-red-400">{uploadError}</div> : null}
    </div>
  );
}

function formatDuration(totalSeconds: number): string {
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
