import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import { useAuthStore } from '@/stores/auth-store';
import {
  useCreateParticle,
  useCreateStreamParticle,
} from '@/hooks/use-create-particle';
import { QuotaExceededError } from '@/lib/errors';
import {
  isUsageExhausted,
  useInvalidateNetworkUsage,
  useNetworkUsage,
} from '@/hooks/use-network-usage';
import { useRecorder } from '@/features/compose/use-recorder';
import { useScreenRecorder } from '@/features/compose/use-screen-recorder';
import { particlePath, parseParticlePath } from '@/lib/particle-path';
import type { ParticlePath } from '@/lib/particle-path';
import { RecordingOverlay } from '@/features/compose/recording-overlay';
import { ScreenSourcePicker } from '@/components/screen-source-picker';
import { KeyHint } from '@/components/key-hint';
import { TextComposeStep } from '@/features/compose/text-compose-step';
import { TaskComposeStep } from '@/features/compose/task-compose-step';
import { ConfigureContainerStep } from '@/features/compose/configure-container-step';
import type { TaskProperties } from '@/api/types';
import { apiClient } from '@/api/client';
import { useMediaSettingsStore } from '@/stores/media-settings-store';
import { useMediaDevicesStore } from '@/stores/media-devices-store';
import { useMediaDevices } from '@/hooks/use-media-devices';
import { resolveEffectiveDeviceId } from '@/hooks/use-effective-device-id';
import { useFileInput } from '@/hooks/use-file-input';
import { createImageThumbnail } from '@/lib/image-thumbnail';
import { MAX_ATTACHMENT_SIZE_BYTES, MAX_ATTACHMENTS } from '@/lib/constants';
import type { PendingAttachment } from '@/features/compose/attachment-strip';
import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
import { useComposeIntentStore } from '@/stores/compose-intent-store';
import { platform } from '@/lib/platform';
import { requireDesktop } from '@/lib/platform/desktop-only';

export type ComposeStep =
  | 'idle'
  | 'picking'
  | 'recording'
  | 'reviewing'
  | 'typing'
  | 'task'
  | 'configuring'
  | 'submitting';

type RecordingSource = 'media' | 'screen';

type PendingArtifact = { type: 'task'; properties: TaskProperties };

interface ComposeOverlayProps {
  networkId: string;
  // Optional target path for reply mode. If not provided, compose creates a new stream.
  targetPath?: ParticlePath;
  onActiveChange?: (active: boolean) => void;
  onStepChange?: (step: ComposeStep) => void;
  onParticleCreated?: (particleId: string) => void;
}

const HOLD_THRESHOLD_MS = 250;

/**
 * Self-contained compose overlay. Each consumer renders its own instance
 * with props that determine the mode (new stream vs. reply).
 */
export function ComposeOverlay({
  networkId,
  targetPath,
  onActiveChange,
  onStepChange,
  onParticleCreated,
}: ComposeOverlayProps) {
  const [step, setStep] = useState<ComposeStep>('idle');
  const [error, setError] = useState<string | null>(null);
  const [textContent, setTextContent] = useState('');
  const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
  const [reviewBlob, setReviewBlob] = useState<Blob | null>(null);
  const [reviewDurationMs, setReviewDurationMs] = useState(0);
  const [reviewMimeType, setReviewMimeType] = useState<string | null>(null);

  const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
  const [recordingSource, setRecordingSource] =
    useState<RecordingSource>('media');

  const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
  const savedMic = useMediaDevicesStore((s) => s.mic);
  const savedCamera = useMediaDevicesStore((s) => s.camera);
  const { audioInputs, videoInputs } = useMediaDevices();
  const micDeviceId = resolveEffectiveDeviceId(savedMic, audioInputs);
  const cameraDeviceId = resolveEffectiveDeviceId(savedCamera, videoInputs);
  const userId = useAuthStore((s) => s.user?.id);
  const createParticle = useCreateParticle();
  const createStream = useCreateStreamParticle();
  const { data: usage } = useNetworkUsage(networkId);
  const invalidateUsage = useInvalidateNetworkUsage();
  const quotaExhausted = isUsageExhausted(usage);

  // Latest props/state for synchronous reads in keyboard handlers.
  const stepRef = useRef(step);
  const recordStartRef = useRef(0);
  const quotaExhaustedRef = useRef(quotaExhausted);
  const recordingSourceRef = useRef(recordingSource);

  const pendingArtifactRef = useRef<PendingArtifact | null>(null);
  useEffect(() => {
    quotaExhaustedRef.current = quotaExhausted;
    recordingSourceRef.current = recordingSource;
  }, [quotaExhausted, recordingSource]);

  const setStepSync = useCallback((next: ComposeStep) => {
    stepRef.current = next;
    setStep(next);
  }, []);

  useSuspendPlayback(step !== 'idle', 'compose');

  // Notify parent when active state changes
  useEffect(() => {
    onActiveChange?.(step !== 'idle');
    onStepChange?.(step);
    // Refresh quota when the overlay activates — user is about to send, so
    // we want the most accurate count before the client-side gate kicks in.
    if (step !== 'idle') {
      void invalidateUsage(networkId);
    }
  }, [step, onActiveChange, onStepChange, invalidateUsage, networkId]);

  const revokeAttachmentThumbnails = useCallback(
    (items: PendingAttachment[]) => {
      for (const a of items) {
        if (a.thumbnailUrl) URL.revokeObjectURL(a.thumbnailUrl);
      }
    },
    [],
  );

  const cancel = useCallback(() => {
    setStepSync('idle');
    setError(null);
    setMediaStream(null);
    setReviewBlob(null);
    setReviewDurationMs(0);
    setReviewMimeType(null);
    setRecordingSource('media');
    pendingArtifactRef.current = null;
    setAttachments((prev) => {
      revokeAttachmentThumbnails(prev);
      return [];
    });
  }, [setStepSync, revokeAttachmentThumbnails]);

  const addAttachments = useCallback(
    async (files: File[]) => {
      const available = MAX_ATTACHMENTS - attachments.length;
      if (available <= 0) {
        toast.error(`Maximum ${MAX_ATTACHMENTS} attachments`);
        return;
      }

      const accepted = files.slice(0, available);
      if (accepted.length < files.length) {
        toast.error(
          `Maximum ${MAX_ATTACHMENTS} attachments — ${files.length - accepted.length} skipped`,
        );
      }

      const newAttachments: PendingAttachment[] = [];
      for (const file of accepted) {
        if (file.size > MAX_ATTACHMENT_SIZE_BYTES) {
          toast.error(`${file.name} is too large (max 25 MB)`);
          continue;
        }
        const thumbnailUrl = await createImageThumbnail(file);
        newAttachments.push({
          id: crypto.randomUUID(),
          file,
          thumbnailUrl,
          status: 'pending',
        });
      }

      if (newAttachments.length > 0) {
        setAttachments((prev) => [...prev, ...newAttachments]);
      }
    },
    [attachments.length],
  );

  const removeAttachment = useCallback((id: string) => {
    setAttachments((prev) => {
      const removed = prev.find((a) => a.id === id);
      if (removed?.thumbnailUrl) URL.revokeObjectURL(removed.thumbnailUrl);
      return prev.filter((a) => a.id !== id);
    });
  }, []);

  const { openFilePicker, isDragging, dropZoneProps } = useFileInput({
    onFilesSelected: addAttachments,
    enabled: step === 'typing' || step === 'reviewing',
  });

  const { startRecording, stopRecording, cancelRecording } = useRecorder({
    mode: recordingMode,
    micDeviceId,
    cameraDeviceId,
    onStreamReady: (stream) => setMediaStream(stream),
    onStreamCleanup: () => setMediaStream(null),
    onFinish: (blob, durationMs, mimeType) => {
      setStepSync('reviewing');
      setReviewBlob(blob);
      setReviewDurationMs(durationMs);
      setReviewMimeType(mimeType);
    },
    onError: (message) => setError(message),
  });

  const {
    startRecording: startScreenRecording,
    stopRecording: stopScreenRecording,
    cancelRecording: cancelScreenRecording,
  } = useScreenRecorder({
    micDeviceId,
    onFinish: (blob, durationMs, mimeType) => {
      setStepSync('reviewing');
      setReviewBlob(blob);
      setReviewDurationMs(durationMs);
      setReviewMimeType(mimeType);
    },
    onError: (message) => {
      setError(message);
      cancel();
    },
  });

  // --- Submission ---

  const uploadMedia = useCallback(
    async (blob: Blob, mimeType: string) => {
      const ext = 'webm';
      const fileName = `recording-${Date.now()}.${ext}`;

      const { object_id, upload_url, upload_headers } =
        await apiClient.prepareUpload({
          network_id: networkId,
          name: fileName,
          content_type: mimeType,
          content_length: blob.size,
        });

      await fetch(upload_url, {
        method: 'PUT',
        headers: upload_headers,
        body: blob,
      });

      await apiClient.confirmUpload(object_id);

      return { object_id, size_bytes: blob.size };
    },
    [networkId],
  );

  const uploadFile = useCallback(
    async (file: File) => {
      const { object_id, upload_url, upload_headers } =
        await apiClient.prepareUpload({
          network_id: networkId,
          name: file.name,
          content_type: file.type || 'application/octet-stream',
          content_length: file.size,
        });

      await fetch(upload_url, {
        method: 'PUT',
        headers: upload_headers,
        body: file,
      });

      await apiClient.confirmUpload(object_id);

      return { object_id, size_bytes: file.size };
    },
    [networkId],
  );

  const uploadAttachments = useCallback(
    async (parentPath: ParticlePath, parentId: string) => {
      if (attachments.length === 0 || !userId) return;

      const { networkId: netId, segments } = parseParticlePath(parentPath);
      const childrenPath = particlePath(netId, [...segments, parentId]);

      const results = await Promise.allSettled(
        attachments.map(async (attachment) => {
          setAttachments((prev) =>
            prev.map((a) =>
              a.id === attachment.id
                ? { ...a, status: 'uploading' as const }
                : a,
            ),
          );

          const { object_id } = await uploadFile(attachment.file);

          await createParticle.mutateAsync({
            path: childrenPath,
            type: 'file',
            properties: {
              object_id,
              filename: attachment.file.name,
              mime_type: attachment.file.type || 'application/octet-stream',
              size_bytes: attachment.file.size,
            },
            createdByHumanId: userId,
          });
        }),
      );

      const failed = results.filter((r) => r.status === 'rejected');
      if (failed.length > 0) {
        toast.error(
          `${failed.length} attachment${failed.length > 1 ? 's' : ''} failed to upload`,
        );
      }
    },
    [attachments, userId, uploadFile, createParticle],
  );

  const createChildParticle = useCallback(
    async (path: ParticlePath) => {
      if (!userId) return;

      let particleId: undefined | string;
      const pendingArtifact = pendingArtifactRef.current;
      if (pendingArtifact) {
        particleId = await createParticle.mutateAsync({
          path,
          type: pendingArtifact.type,
          properties: pendingArtifact.properties,
          createdByHumanId: userId,
        });
      } else if (textContent.trim()) {
        particleId = await createParticle.mutateAsync({
          path,
          type: 'text',
          properties: { content: textContent },
          createdByHumanId: userId,
        });
      } else if (reviewBlob && reviewMimeType) {
        const { object_id, size_bytes } = await uploadMedia(
          reviewBlob,
          reviewMimeType,
        );

        const isAudioOnly = reviewMimeType.startsWith('audio/');
        particleId = await createParticle.mutateAsync({
          path,
          type: 'media',
          properties: {
            object_id,
            mime_type: reviewMimeType,
            duration_ms: reviewDurationMs,
            size_bytes,
            ...(!isAudioOnly && {
              source:
                recordingSource === 'screen'
                  ? ('screen' as const)
                  : ('camera' as const),
            }),
          },
          createdByHumanId: userId,
        });
      }

      if (particleId) {
        await uploadAttachments(path, particleId);
        onParticleCreated?.(particleId);
      }
    },
    [
      userId,
      textContent,
      reviewBlob,
      reviewMimeType,
      reviewDurationMs,
      recordingSource,
      createParticle,
      uploadMedia,
      uploadAttachments,
      onParticleCreated,
    ],
  );

  const handleQuotaError = useCallback(
    (err: unknown): boolean => {
      if (err instanceof QuotaExceededError) {
        toast.error(
          'Daily message limit reached. Upgrade to Pro to keep sending.',
        );
        cancel();
        return true;
      }
      return false;
    },
    [cancel],
  );

  // Reply mode: create particle directly under targetPath.
  const onSubmitReply = useCallback(async () => {
    if (!targetPath || !userId || stepRef.current === 'submitting') return;
    setStepSync('submitting');
    try {
      await createChildParticle(targetPath);
      cancel();
      setTextContent('');
    } catch (err) {
      if (!handleQuotaError(err)) throw err;
    }
  }, [
    targetPath,
    userId,
    setStepSync,
    createChildParticle,
    cancel,
    handleQuotaError,
  ]);

  // New stream mode: create stream + first child
  const handleStreamSubmit = useCallback(
    async (streamName: string, visibleTo: string[]) => {
      if (!userId || stepRef.current === 'submitting') return;
      setStepSync('submitting');

      try {
        const streamId = await createStream.mutateAsync({
          networkId,
          properties: {
            name: streamName,
          },
          createdByHumanId: userId,
          visibleTo,
        });

        const streamChildrenPath = particlePath(networkId, [streamId]);
        await createChildParticle(streamChildrenPath);
        cancel();
        setTextContent('');
      } catch (err) {
        if (!handleQuotaError(err)) throw err;
      }
    },
    [
      networkId,
      userId,
      createStream,
      createChildParticle,
      cancel,
      handleQuotaError,
      setStepSync,
    ],
  );

  // --- Compose intent handlers ---
  // Single source of truth for the step transitions triggered by the user.
  // Both the keyboard handler and the intent store dispatch into these so
  // guards (quota) and screen-vs-media branching live in one place.

  const guardIdle = useCallback((): boolean => {
    if (stepRef.current !== 'idle') return false;
    if (quotaExhaustedRef.current) {
      toast.info(
        'Daily message limit reached. Upgrade to Pro to keep sending.',
      );
      return false;
    }
    return true;
  }, []);

  const handleRecordIntent = useCallback(() => {
    if (!guardIdle()) return;
    recordStartRef.current = Date.now();
    setRecordingSource('media');
    setStepSync('recording');
    startRecording();
  }, [guardIdle, setStepSync, startRecording]);

  const handleTextIntent = useCallback(() => {
    if (!guardIdle()) return;
    setStepSync('typing');
  }, [guardIdle, setStepSync]);

  const handleTaskIntent = useCallback(() => {
    if (!guardIdle()) return;
    setStepSync('task');
  }, [guardIdle, setStepSync]);

  // Artifact submit: capture the artifact, then reuse the standard flow —
  // reply mode creates it under targetPath, root mode configures a stream
  // that will hold it as its first child.
  const handleArtifactSubmit = useCallback(
    (artifact: PendingArtifact) => {
      pendingArtifactRef.current = artifact;
      if (targetPath) {
        void onSubmitReply();
      } else {
        setStepSync('configuring');
      }
    },
    [targetPath, onSubmitReply, setStepSync],
  );

  const handleStopIntent = useCallback(() => {
    if (stepRef.current !== 'recording') return;
    if (recordingSourceRef.current === 'screen') {
      stopScreenRecording();
    } else {
      stopRecording();
    }
  }, [stopRecording, stopScreenRecording]);

  const handleCancelIntent = useCallback(() => {
    const s = stepRef.current;
    if (s === 'recording' || s === 'reviewing') {
      if (recordingSourceRef.current === 'screen') {
        cancelScreenRecording();
      } else {
        cancelRecording();
      }
      cancel();
    } else if (s === 'typing' || s === 'configuring' || s === 'picking') {
      cancel();
    }
  }, [cancel, cancelRecording, cancelScreenRecording]);

  const handleSendIntent = useCallback(() => {
    if (stepRef.current !== 'reviewing') return;
    if (targetPath) {
      onSubmitReply();
    } else {
      setStepSync('configuring');
    }
  }, [targetPath, onSubmitReply, setStepSync]);

  // --- Intent store subscription ---
  // External callers (clickable hints) dispatch via the store; this overlay
  // executes the matching handler and clears the intent. Keyboard handlers
  // call the same handlers directly without a store round-trip.

  const clearIntent = useComposeIntentStore((s) => s.clear);

  // Consume fire-and-forget intents from the external store. Reacting in the
  // store subscription (not an effect body) keeps these state-updating handlers
  // off the render path and avoids an extra dispatch→render bounce.
  useEffect(() => {
    return useComposeIntentStore.subscribe((state, prev) => {
      const intent = state.intent;
      if (!intent || intent === prev.intent) return;
      switch (intent.kind) {
        case 'record':
          handleRecordIntent();
          break;
        case 'text':
          handleTextIntent();
          break;
        case 'task':
          handleTaskIntent();
          break;
        case 'stop':
          handleStopIntent();
          break;
        case 'cancel':
          handleCancelIntent();
          break;
        case 'send':
          handleSendIntent();
          break;
      }
      clearIntent();
    });
  }, [
    handleRecordIntent,
    handleTextIntent,
    handleTaskIntent,
    handleStopIntent,
    handleCancelIntent,
    handleSendIntent,
    clearIntent,
  ]);

  // --- Keyboard handling ---

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const currentStep = stepRef.current;

      if (
        currentStep === 'typing' ||
        currentStep === 'task' ||
        currentStep === 'configuring' ||
        currentStep === 'picking'
      ) {
        if (e.key === 'Escape') {
          e.preventDefault();
          cancel();
        }
        return;
      }

      const target = e.target as HTMLElement;
      if (
        target.tagName === 'INPUT' ||
        target.tagName === 'TEXTAREA' ||
        target.isContentEditable
      ) {
        return;
      }

      switch (currentStep) {
        case 'idle': {
          if (e.key === '`' && !e.repeat) {
            e.preventDefault();
            handleRecordIntent();
          } else if (e.key === 's' || e.key === 'S') {
            e.preventDefault();
            if (!guardIdle()) break;
            if (!requireDesktop('Screen recording')) break;
            setRecordingSource('screen');
            setStepSync('picking');
          } else if (e.key === 't' || e.key === 'T') {
            e.preventDefault();
            handleTextIntent();
          } else if (e.key === 'd' || e.key === 'D') {
            e.preventDefault();
            handleTaskIntent();
          }
          break;
        }

        case 'recording': {
          if (e.key === '`' && !e.repeat) {
            // Second tap stops media recording (toggle mode)
            e.preventDefault();
            handleStopIntent();
          } else if (
            (e.key === 's' || e.key === 'S') &&
            recordingSourceRef.current === 'screen'
          ) {
            // S stops screen recording when main window is focused
            e.preventDefault();
            handleStopIntent();
          } else if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
            e.preventDefault();
            handleCancelIntent();
          }
          break;
        }

        case 'reviewing': {
          if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
            e.preventDefault();
            handleCancelIntent();
          } else if (e.key === 'Enter') {
            e.preventDefault();
            handleSendIntent();
          }
          break;
        }
      }
    };

    const handleKeyUp = (e: KeyboardEvent) => {
      if (
        stepRef.current === 'recording' &&
        e.key === '`' &&
        recordingSourceRef.current === 'media'
      ) {
        e.preventDefault();
        // Only stop on release if held long enough (hold-to-record mode).
        // Quick taps are handled by the second keydown (toggle mode).
        if (
          recordStartRef.current > 0 &&
          Date.now() - recordStartRef.current >= HOLD_THRESHOLD_MS
        ) {
          handleStopIntent();
          recordStartRef.current = 0;
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, [
    cancel,
    setStepSync,
    guardIdle,
    handleRecordIntent,
    handleTextIntent,
    handleTaskIntent,
    handleStopIntent,
    handleCancelIntent,
    handleSendIntent,
  ]);

  // --- Screen source selection handler ---

  const handleScreenSourceSelected = useCallback(
    (sourceId: string) => {
      setStepSync('recording');
      startScreenRecording(sourceId);
    },
    [setStepSync, startScreenRecording],
  );

  // --- Render ---

  if (step === 'idle') return null;

  const handleTextAdvance = targetPath
    ? onSubmitReply
    : () => setStepSync('configuring');

  return (
    <>
      {step === 'picking' && (
        <ScreenSourcePicker
          title="Record your screen"
          confirmLabel="Record"
          getSources={platform.screenRecord.getScreenSources}
          onSelect={handleScreenSourceSelected}
          onCancel={cancel}
        />
      )}
      {(step === 'recording' || step === 'reviewing') &&
        recordingSource === 'media' && (
          <RecordingOverlay
            step={step}
            mediaStream={mediaStream}
            recordingMode={recordingMode}
            reviewBlob={reviewBlob}
            error={error}
            onClose={cancel}
            attachments={attachments}
            onRemoveAttachment={removeAttachment}
            onAddFiles={openFilePicker}
            isDragging={isDragging}
            dropZoneProps={dropZoneProps}
            mirror={true}
            objectFit="cover"
          />
        )}
      {step === 'recording' && recordingSource === 'screen' && (
        <div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
          <div className="absolute top-8 z-10">
            <div className="flex items-center gap-2">
              <span className="h-2.5 w-2.5 animate-pulse rounded-full bg-red-500" />
              <span className="text-sm text-white/80">Recording screen…</span>
            </div>
          </div>
          <div className="absolute bottom-4 z-10 flex items-center gap-4 text-sm text-white/50">
            <KeyHint
              keys="S"
              onClick={handleStopIntent}
              title="Stop screen recording (or press S)"
            >
              stop
            </KeyHint>
            <KeyHint
              keys="Q"
              onClick={handleCancelIntent}
              title="Cancel screen recording (or press Q)"
            >
              cancel
            </KeyHint>
          </div>
        </div>
      )}
      {step === 'reviewing' && recordingSource === 'screen' && reviewBlob && (
        <RecordingOverlay
          step="reviewing"
          mediaStream={null}
          recordingMode="video"
          reviewBlob={reviewBlob}
          error={error}
          onClose={cancel}
          attachments={attachments}
          onRemoveAttachment={removeAttachment}
          onAddFiles={openFilePicker}
          isDragging={isDragging}
          dropZoneProps={dropZoneProps}
          mirror={false}
          objectFit="contain"
        />
      )}
      {step === 'typing' && (
        <TextComposeStep
          textContent={textContent}
          onTextChange={setTextContent}
          onAdvance={handleTextAdvance}
          onCancel={cancel}
          attachments={attachments}
          onRemoveAttachment={removeAttachment}
          onAddFiles={openFilePicker}
          isDragging={isDragging}
          dropZoneProps={dropZoneProps}
        />
      )}
      {step === 'task' && (
        <TaskComposeStep
          networkId={networkId}
          onCancel={cancel}
          onSubmit={(properties) =>
            handleArtifactSubmit({ type: 'task', properties })
          }
        />
      )}
      {!targetPath && step === 'configuring' && (
        <ConfigureContainerStep
          networkId={networkId}
          onCancel={cancel}
          onSubmit={handleStreamSubmit}
        />
      )}
      {step === 'submitting' && (
        <div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
          <span className="animate-pulse text-sm text-white/60">
            Sending...
          </span>
        </div>
      )}
    </>
  );
}
