import { useCallback, useEffect, useRef, useState } from 'react';
import { Camera, Loader2, Trash2, Upload } from 'lucide-react';
import { toast } from 'sonner';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Muted } from '@/components/ui/typography';
import { apiClient } from '@/api/client';
import { useAuthStore } from '@/stores/auth-store';
import { useMediaDevicesStore } from '@/stores/media-devices-store';
import { useAvatarUrl } from '@/hooks/use-avatar-url';
import { useFileInput } from '@/hooks/use-file-input';
import { toAvatarBlob } from '@/lib/avatar-image';
import { logError, toUserMessage } from '@/lib/errors';
import { cn } from '@/lib/utils';

// Guard the *source* file before decoding so we never load a huge image into
// memory just to throw most of it away — the uploaded blob is always our small
// re-encoded square regardless of input size.
const MAX_SOURCE_BYTES = 30 * 1024 * 1024;

interface AvatarEditDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export function AvatarEditDialog({
  open,
  onOpenChange,
}: AvatarEditDialogProps) {
  const user = useAuthStore((s) => s.user);
  const refreshUser = useAuthStore((s) => s.refreshUser);
  const currentUrl = useAvatarUrl(user?.avatar_object_id);
  const initials = user?.email_prefix?.slice(0, 2).toUpperCase() ?? '?';
  const hasAvatar = !!user?.avatar_object_id;

  const [tab, setTab] = useState<'upload' | 'camera'>('upload');
  const [prepared, setPrepared] = useState<{ blob: Blob; url: string } | null>(
    null,
  );
  const [busy, setBusy] = useState(false);

  // Keep the latest prepared blob in a ref so the unmount cleanup can revoke
  // its object URL without re-running on every change.
  const preparedRef = useRef(prepared);
  useEffect(() => {
    preparedRef.current = prepared;
  }, [prepared]);
  useEffect(
    () => () => {
      if (preparedRef.current) URL.revokeObjectURL(preparedRef.current.url);
    },
    [],
  );

  const setPreparedFromBlob = useCallback((blob: Blob) => {
    setPrepared((prev) => {
      if (prev) URL.revokeObjectURL(prev.url);
      return { blob, url: URL.createObjectURL(blob) };
    });
  }, []);

  // Close and reset to a clean slate so reopening starts fresh.
  const close = useCallback(() => {
    setPrepared((prev) => {
      if (prev) URL.revokeObjectURL(prev.url);
      return null;
    });
    setTab('upload');
    setBusy(false);
    onOpenChange(false);
  }, [onOpenChange]);

  const handleOpenChange = useCallback(
    (next: boolean) => {
      if (next) onOpenChange(true);
      else close();
    },
    [close, onOpenChange],
  );

  const handleFiles = useCallback(
    async (files: File[]) => {
      const file = files[0];
      if (!file) return;
      if (!file.type.startsWith('image/')) {
        toast.error('Please choose an image file.');
        return;
      }
      if (file.size > MAX_SOURCE_BYTES) {
        toast.error('That image is too large — choose one under 30MB.');
        return;
      }
      try {
        const bitmap = await createImageBitmap(file);
        const blob = await toAvatarBlob(bitmap);
        bitmap.close();
        setPreparedFromBlob(blob);
      } catch (err) {
        toast.error('Could not process that image.');
        logError(err, { scope: 'avatar.processFile' });
      }
    },
    [setPreparedFromBlob],
  );

  const { openFilePicker, isDragging, dropZoneProps } = useFileInput({
    onFilesSelected: handleFiles,
    enabled: open && tab === 'upload',
  });

  const handleSave = async () => {
    if (!prepared) return;
    setBusy(true);
    try {
      await apiClient.updateAvatar(prepared.blob);
      await refreshUser();
      toast.success('Avatar updated');
      close();
    } catch (err) {
      toast.error(toUserMessage(err));
      logError(err, { scope: 'avatar.save' });
      setBusy(false);
    }
  };

  const handleRemove = async () => {
    setBusy(true);
    try {
      await apiClient.deleteAvatar();
      await refreshUser();
      toast.success('Avatar removed');
      close();
    } catch (err) {
      toast.error(toUserMessage(err));
      logError(err, { scope: 'avatar.remove' });
      setBusy(false);
    }
  };

  const previewUrl = prepared?.url ?? currentUrl;

  return (
    <Dialog open={open} onOpenChange={handleOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Profile picture</DialogTitle>
          <DialogDescription>
            Upload an image or take one with your camera.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col items-center gap-4 py-1">
          <div className="bg-muted flex size-24 items-center justify-center overflow-hidden rounded-full border">
            {previewUrl ? (
              <img src={previewUrl} alt="" className="size-full object-cover" />
            ) : (
              <span className="text-muted-foreground text-2xl font-medium">
                {initials}
              </span>
            )}
          </div>

          <Tabs
            value={tab}
            onValueChange={(v) => setTab(v as 'upload' | 'camera')}
            className="w-full"
          >
            <TabsList className="w-full">
              <TabsTrigger value="upload">
                <Upload />
                Upload
              </TabsTrigger>
              <TabsTrigger value="camera">
                <Camera />
                Take photo
              </TabsTrigger>
            </TabsList>

            <TabsContent value="upload" className="pt-3">
              <div
                {...dropZoneProps}
                className={cn(
                  'flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-6 py-5 text-center transition-colors',
                  isDragging && 'border-primary bg-primary/5',
                )}
              >
                <Muted className="text-xs">Drag an image here, or</Muted>
                <Button variant="outline" size="sm" onClick={openFilePicker}>
                  Choose image
                </Button>
              </div>
            </TabsContent>

            <TabsContent value="camera" className="pt-3">
              <CameraCapture
                active={open && tab === 'camera'}
                onCapture={setPreparedFromBlob}
              />
            </TabsContent>
          </Tabs>
        </div>

        <DialogFooter>
          {hasAvatar && (
            <Button
              variant="ghost"
              size="sm"
              className="text-destructive hover:text-destructive sm:mr-auto"
              onClick={handleRemove}
              disabled={busy}
            >
              <Trash2 className="mr-1 size-3.5" />
              Remove
            </Button>
          )}
          <Button variant="outline" size="sm" onClick={close} disabled={busy}>
            Cancel
          </Button>
          <Button size="sm" onClick={handleSave} disabled={!prepared || busy}>
            {busy && <Loader2 className="mr-1 size-3.5 animate-spin" />}
            Save
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function CameraCapture({
  active,
  onCapture,
}: {
  active: boolean;
  onCapture: (blob: Blob) => void;
}) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [stream, setStream] = useState<MediaStream | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [capturing, setCapturing] = useState(false);
  // Honor the camera the user picked in Audio & Video settings. `ideal` rather
  // than `exact` so a since-unplugged device falls back to the default instead
  // of throwing OverconstrainedError.
  const savedCameraId = useMediaDevicesStore((s) => s.camera?.deviceId);

  useEffect(() => {
    if (!active) return;

    let cancelled = false;
    let acquired: MediaStream | null = null;

    const video: MediaTrackConstraints = { aspectRatio: { ideal: 1 } };
    if (savedCameraId) video.deviceId = { ideal: savedCameraId };

    navigator.mediaDevices
      .getUserMedia({ video, audio: false })
      .then((s) => {
        if (cancelled) {
          s.getTracks().forEach((t) => t.stop());
          return;
        }
        acquired = s;
        setStream(s);
        setError(null);
      })
      .catch((err: unknown) => {
        if (cancelled) return;
        setError(
          err instanceof Error ? err.message : 'Unable to access camera',
        );
      });

    return () => {
      cancelled = true;
      acquired?.getTracks().forEach((t) => t.stop());
      setStream(null);
    };
  }, [active, savedCameraId]);

  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = stream;
  }, [stream]);

  const handleCapture = async () => {
    const video = videoRef.current;
    // readyState < HAVE_CURRENT_DATA (or zero dimensions) means no frame has
    // decoded yet — capturing now would grab a blank image.
    if (!video || video.readyState < 2 || !video.videoWidth) {
      toast.error('Camera is still starting — try again in a moment.');
      return;
    }
    setCapturing(true);
    try {
      const bitmap = await createImageBitmap(video);
      // Mirror to match the (mirrored) live preview the user is looking at.
      const blob = await toAvatarBlob(bitmap, { mirror: true });
      bitmap.close();
      onCapture(blob);
    } catch (err) {
      toast.error('Could not capture photo.');
      logError(err, { scope: 'avatar.capture' });
    } finally {
      setCapturing(false);
    }
  };

  if (error) {
    return (
      <div className="flex flex-col items-center gap-1 rounded-lg border border-dashed px-6 py-5 text-center">
        <Muted className="text-xs">Couldn't access your camera.</Muted>
        <Muted className="text-[11px]">{error}</Muted>
      </div>
    );
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <div className="bg-muted size-36 overflow-hidden rounded-full border">
        <video
          ref={videoRef}
          muted
          autoPlay
          playsInline
          className="size-full -scale-x-100 object-cover"
        />
      </div>
      <Button size="sm" onClick={handleCapture} disabled={!stream || capturing}>
        <Camera className="mr-1 size-3.5" />
        Capture
      </Button>
    </div>
  );
}
