"use client";

import { Button } from "@/components/ui/button";
import { useOptionalFileUploadSettings } from "@/contexts/settings-context";
import { cn } from "@/lib/utils";
import { FileIcon, FileUp, Upload, X } from "lucide-react";
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";

type FileEntry = {
  id: string;
  file: File;
  previewUrl: string | null;
};

export interface ExternalPreviewItem {
  id: string;
  name: string;
  previewUrl: string | null;
}

export type UploaderMode = "dropzone" | "trigger";
export type UploaderTone = "default" | "primary";
export type PreviewVariant = "none" | "grid" | "list";
export type RejectionReason = "max_size" | "invalid_type" | "max_count";

export interface RejectedFile {
  file: File;
  reason: RejectionReason;
}

export interface UnifiedFileUploaderProps {
  mode?: UploaderMode;
  tone?: UploaderTone;
  value?: File[];
  externalPreviewItems?: ExternalPreviewItem[];
  onChange: (files: File[]) => void;
  multiple?: boolean;
  accept?: string;
  maxSizeMb?: number;
  maxFiles?: number;
  useGlobalSettings?: boolean;
  disabled?: boolean;
  previewVariant?: PreviewVariant;
  labels: {
    dragText?: string;
    browseText: string;
    maxSizeText?: string;
    selectedFilesText?: string;
    orText?: string;
    secondaryActionText?: string;
  };
  secondaryAction?: {
    onClick: () => void;
    icon?: ReactNode;
  };
  triggerContent?: ReactNode;
  onRejectedFiles?: (rejected: RejectedFile[]) => void;
  showRejectionToasts?: boolean;
  className?: string;
}

function parseAcceptTokens(accept?: string): string[] {
  if (!accept) return [];
  return accept
    .split(",")
    .map((part) => part.trim().toLowerCase())
    .filter(Boolean);
}

function isFileAccepted(file: File, acceptedTokens: string[]): boolean {
  if (acceptedTokens.length === 0) return true;

  const fileType = file.type.toLowerCase();
  const extension = `.${file.name.split(".").pop()?.toLowerCase() || ""}`;

  return acceptedTokens.some((token) => {
    if (token.startsWith(".")) {
      return extension === token;
    }
    if (token.endsWith("/*")) {
      return fileType.startsWith(token.slice(0, -1));
    }
    return fileType === token;
  });
}

function formatFileSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

function formatMegabytes(value: number): string {
  return Number.isInteger(value) ? String(value) : value.toFixed(1);
}

function truncateFileName(name: string, maxLength: number = 30): string {
  if (name.length <= maxLength) return name;
  const extension = name.split(".").pop();
  const nameWithoutExtension = name.substring(0, name.lastIndexOf("."));
  const charsToShow = maxLength - (extension?.length || 0) - 4;
  return `${nameWithoutExtension.substring(0, charsToShow)}...${extension}`;
}

function generateId(): string {
  return `upload-${Math.random().toString(36).slice(2, 11)}`;
}

function mapFilesToEntries(
  files: File[],
  previousEntries: FileEntry[],
  previewVariant: PreviewVariant,
): FileEntry[] {
  const previousMap = new Map(previousEntries.map((entry) => [entry.file, entry]));
  const shouldRenderImagePreview = previewVariant === "grid";

  return files.map((file) => {
    const previousEntry = previousMap.get(file);
    if (previousEntry) {
      if (shouldRenderImagePreview) {
        if (previousEntry.previewUrl || file.type.startsWith("image/")) {
          return {
            ...previousEntry,
            previewUrl:
              previousEntry.previewUrl ||
              (file.type.startsWith("image/")
                ? URL.createObjectURL(file)
                : null),
          };
        }
      }

      return {
        ...previousEntry,
        previewUrl: shouldRenderImagePreview ? previousEntry.previewUrl : null,
      };
    }

    return {
      id: generateId(),
      file,
      previewUrl:
        shouldRenderImagePreview && file.type.startsWith("image/")
          ? URL.createObjectURL(file)
          : null,
    };
  });
}

export function UnifiedFileUploader({
  mode = "dropzone",
  tone = "default",
  value,
  externalPreviewItems = [],
  onChange,
  multiple = true,
  accept,
  maxSizeMb,
  maxFiles,
  useGlobalSettings = true,
  disabled = false,
  previewVariant = "grid",
  labels,
  secondaryAction,
  triggerContent,
  onRejectedFiles,
  showRejectionToasts = true,
  className,
}: UnifiedFileUploaderProps) {
  const isControlled = value !== undefined;
  const fileInputRef = useRef<HTMLInputElement>(null);
  const previewUrlsRef = useRef<string[]>([]);
  const [isDragging, setIsDragging] = useState(false);
  const [entries, setEntries] = useState<FileEntry[]>([]);
  const globalSettings = useOptionalFileUploadSettings();
  const hasGlobalLimits = Boolean(globalSettings);

  const globalMaxSize = globalSettings?.maxFileSize ?? 5;
  const globalMaxFilesCount = globalSettings?.maxFilesCount ?? 10;
  const globalExtensions = useMemo(
    () =>
      (globalSettings?.allowedExtensions ?? []).map((ext) =>
        ext.startsWith(".") ? ext : `.${ext}`,
      ),
    [globalSettings?.allowedExtensions],
  );

  const resolvedAccept = useMemo(() => {
    if (accept && accept.trim().length > 0) {
      return accept;
    }

    if (useGlobalSettings && globalExtensions.length > 0) {
      return globalExtensions.join(",");
    }

    return "";
  }, [accept, globalExtensions, useGlobalSettings]);

  const acceptedTokens = useMemo(
    () => parseAcceptTokens(resolvedAccept),
    [resolvedAccept],
  );

  const resolvedMaxSizeMb = useMemo(() => {
    const localMaxSizeMb =
      maxSizeMb !== undefined
        ? maxSizeMb
        : useGlobalSettings
          ? globalMaxSize
          : undefined;

    if (!hasGlobalLimits) {
      return localMaxSizeMb;
    }

    if (localMaxSizeMb === undefined) {
      return globalMaxSize;
    }

    return Math.min(localMaxSizeMb, globalMaxSize);
  }, [globalMaxSize, hasGlobalLimits, maxSizeMb, useGlobalSettings]);

  const resolvedMaxFiles = useMemo(() => {
    if (!multiple) return 1;

    const localMaxFiles =
      maxFiles !== undefined
        ? maxFiles
        : useGlobalSettings
          ? globalMaxFilesCount
          : undefined;

    if (!hasGlobalLimits) {
      return localMaxFiles;
    }

    if (localMaxFiles === undefined) {
      return globalMaxFilesCount;
    }

    return Math.min(localMaxFiles, globalMaxFilesCount);
  }, [
    globalMaxFilesCount,
    hasGlobalLimits,
    maxFiles,
    multiple,
    useGlobalSettings,
  ]);

  useEffect(() => {
    if (!isControlled) return;
    // Controlled mode: mirror parent state into local display entries.
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setEntries((prev) => mapFilesToEntries(value ?? [], prev, previewVariant));
  }, [isControlled, previewVariant, value]);

  useEffect(() => {
    const previousUrls = previewUrlsRef.current;
    const nextUrls = entries
      .map((entry) => entry.previewUrl)
      .filter((url): url is string => Boolean(url));

    for (const url of previousUrls) {
      if (!nextUrls.includes(url)) {
        URL.revokeObjectURL(url);
      }
    }

    previewUrlsRef.current = nextUrls;
  }, [entries]);

  useEffect(
    () => () => {
      for (const url of previewUrlsRef.current) {
        URL.revokeObjectURL(url);
      }
      previewUrlsRef.current = [];
    },
    [],
  );

  const syncFiles = (nextFiles: File[]) => {
    if (!isControlled) {
      setEntries((prev) => mapFilesToEntries(nextFiles, prev, previewVariant));
    }
    onChange(nextFiles);
  };

  const clearNativeInputValue = () => {
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  };

  const processFiles = (incomingFiles: File[]) => {
    if (incomingFiles.length === 0 || disabled) {
      clearNativeInputValue();
      return;
    }

    const currentFiles = isControlled
      ? (value ?? [])
      : entries.map((entry) => entry.file);
    const acceptedFiles: File[] = [];
    const rejectedFiles: RejectedFile[] = [];

    const maxSizeInBytes =
      resolvedMaxSizeMb !== undefined ? resolvedMaxSizeMb * 1024 * 1024 : null;

    for (const file of incomingFiles) {
      if (maxSizeInBytes !== null && file.size > maxSizeInBytes) {
        rejectedFiles.push({ file, reason: "max_size" });
        continue;
      }

      if (!isFileAccepted(file, acceptedTokens)) {
        rejectedFiles.push({ file, reason: "invalid_type" });
        continue;
      }

      if (multiple) {
        if (
          resolvedMaxFiles !== undefined &&
          currentFiles.length + acceptedFiles.length >= resolvedMaxFiles
        ) {
          rejectedFiles.push({ file, reason: "max_count" });
          continue;
        }
        acceptedFiles.push(file);
      } else {
        if (acceptedFiles.length > 0) {
          rejectedFiles.push({ file, reason: "max_count" });
          continue;
        }
        acceptedFiles.push(file);
      }
    }

    if (rejectedFiles.length > 0) {
      onRejectedFiles?.(rejectedFiles);

      if (showRejectionToasts) {
        const maxSizeRejectedCount = rejectedFiles.filter(
          (item) => item.reason === "max_size",
        ).length;
        const maxCountRejectedCount = rejectedFiles.filter(
          (item) => item.reason === "max_count",
        ).length;
        const invalidTypeRejectedCount = rejectedFiles.filter(
          (item) => item.reason === "invalid_type",
        ).length;

        if (maxSizeRejectedCount > 0) {
          if (resolvedMaxSizeMb !== undefined) {
            toast.error(
              `Some files exceed the ${formatMegabytes(resolvedMaxSizeMb)}MB size limit.`,
            );
          } else {
            toast.error("Some files are too large.");
          }
        }

        if (maxCountRejectedCount > 0) {
          if (resolvedMaxFiles !== undefined) {
            toast.error(
              `You can upload up to ${resolvedMaxFiles} file${resolvedMaxFiles > 1 ? "s" : ""}.`,
            );
          } else {
            toast.error("Too many files selected.");
          }
        }

        if (invalidTypeRejectedCount > 0) {
          toast.error("Some files have unsupported file types.");
        }
      }
    }

    if (acceptedFiles.length === 0) {
      clearNativeInputValue();
      return;
    }

    const nextFiles = multiple
      ? [...currentFiles, ...acceptedFiles]
      : [acceptedFiles[0]];
    syncFiles(nextFiles);
    clearNativeInputValue();
  };

  const handleBrowseClick = () => {
    if (disabled) return;
    fileInputRef.current?.click();
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (!event.target.files) return;
    processFiles(Array.from(event.target.files));
  };

  const handleRemoveFile = (id: string) => {
    if (disabled) return;
    const nextFiles = entries
      .filter((entry) => entry.id !== id)
      .map((entry) => entry.file);
    syncFiles(nextFiles);
  };

  const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    if (!disabled) {
      setIsDragging(true);
    }
  };

  const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    setIsDragging(false);
  };

  const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    setIsDragging(false);
    processFiles(Array.from(event.dataTransfer.files));
  };

  const dropzoneStyles = {
    default: {
      base: "border-border hover:border-primary/50",
      dragging: "border-primary bg-primary/5",
      iconBg: "bg-muted",
      iconBgDragging: "bg-primary/10",
      icon: "text-muted-foreground",
      iconDragging: "text-primary",
      text: "text-foreground",
      helper: "text-muted-foreground",
      browseVariant: "outline" as const,
    },
    primary: {
      base: "border-primary bg-tag-background-success-light",
      dragging: "border-primary bg-tag-background-success-light/80",
      iconBg: "bg-transparent",
      iconBgDragging: "bg-transparent",
      icon: "text-primary",
      iconDragging: "text-primary",
      text: "text-primary",
      helper: "text-primary",
      browseVariant: "secondary" as const,
    },
  };

  const styles = dropzoneStyles[tone];
  const IconComponent = tone === "primary" ? FileUp : Upload;
  const previewItems = useMemo(
    () => [
      ...externalPreviewItems.map((item) => ({
        ...item,
        type: "external" as const,
      })),
      ...entries.map((entry) => ({
        id: entry.id,
        name: entry.file.name,
        previewUrl: entry.previewUrl,
        type: "file" as const,
        file: entry.file,
      })),
    ],
    [entries, externalPreviewItems],
  );

  const renderPreview = () => {
    if (previewVariant === "none" || previewItems.length === 0) {
      return null;
    }

    if (previewVariant === "grid") {
      return (
        <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
          {previewItems.map((item) => (
            <div
              key={item.id}
              className="group relative overflow-hidden rounded-lg border bg-muted/50"
            >
              {item.previewUrl ? (
                <div className="relative aspect-square">
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={item.previewUrl}
                    alt={item.name}
                    className="h-full w-full object-cover"
                  />
                </div>
              ) : (
                <div className="flex aspect-square flex-col items-center justify-center gap-2 bg-muted p-2">
                  <FileIcon className="h-8 w-8 text-muted-foreground" />
                  <span className="line-clamp-2 break-all text-center text-xs text-muted-foreground">
                    {item.name}
                  </span>
                </div>
              )}
              {!disabled && item.type === "file" && (
                <button
                  type="button"
                  onClick={(event) => {
                    event.stopPropagation();
                    handleRemoveFile(item.id);
                  }}
                  className="absolute top-1 end-1 rounded-full bg-destructive p-1.5 text-destructive-foreground opacity-0 transition-opacity hover:bg-destructive/90 group-hover:opacity-100"
                >
                  <X className="h-3 w-3" />
                </button>
              )}
              {item.previewUrl && (
                <div className="absolute inset-x-0 bottom-0 bg-black/60 px-2 py-1">
                  <p className="truncate text-xs text-white">{item.name}</p>
                </div>
              )}
            </div>
          ))}
        </div>
      );
    }

    return (
      <div className="space-y-2">
        {labels.selectedFilesText && (
          <p className="text-sm font-medium">
            {labels.selectedFilesText} ({previewItems.length})
          </p>
        )}
        <div className="min-w-0 space-y-2">
          {previewItems.map((item) => (
            <div
              key={item.id}
              className="flex w-full min-w-0 items-center gap-2 overflow-hidden rounded bg-muted p-2 text-sm"
            >
              <span
                className="min-w-0 flex-1 truncate text-left [direction:ltr]"
                title={item.name}
              >
                {truncateFileName(item.name)}
              </span>
              <span className="shrink-0 text-xs text-muted-foreground [direction:ltr]">
                {item.type === "file"
                  ? `(${formatFileSize(item.file.size)})`
                  : "(existing image)"}
              </span>
              {!disabled && item.type === "file" && (
                <Button
                  type="button"
                  variant="ghost"
                  size="sm"
                  className="h-6 w-6 shrink-0 p-0"
                  onClick={(event) => {
                    event.stopPropagation();
                    handleRemoveFile(item.id);
                  }}
                >
                  <X className="h-4 w-4" />
                </Button>
              )}
            </div>
          ))}
        </div>
      </div>
    );
  };

  if (mode === "trigger") {
    return (
      <div className={className}>
        <input
          ref={fileInputRef}
          type="file"
          className="hidden"
          onChange={handleInputChange}
          multiple={multiple}
          accept={resolvedAccept}
          disabled={disabled}
        />
        <span
          className={cn("inline-flex", disabled && "cursor-not-allowed opacity-50")}
          onClick={(event) => {
            event.preventDefault();
            event.stopPropagation();
            handleBrowseClick();
          }}
          aria-disabled={disabled}
        >
          {triggerContent ?? (
            <Button
              type="button"
              variant="outline"
              size="sm"
              disabled={disabled}
            >
              {labels.browseText}
            </Button>
          )}
        </span>
        {renderPreview()}
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <div
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
        className={cn(
          "cursor-pointer rounded-lg border-2 border-dashed p-6 transition-colors",
          disabled && "cursor-not-allowed opacity-50",
          isDragging ? styles.dragging : styles.base,
          className,
        )}
        onClick={handleBrowseClick}
      >
        <div className="flex flex-col items-center justify-center gap-4 text-center">
          <div
            className={cn(
              "rounded-full p-4 transition-colors",
              isDragging ? styles.iconBgDragging : styles.iconBg,
            )}
          >
            <IconComponent
              className={cn(
                tone === "primary" ? "h-6 w-6" : "h-8 w-8",
                isDragging ? styles.iconDragging : styles.icon,
              )}
            />
          </div>

          <div className="space-y-2">
            {labels.dragText && (
              <p className={cn("text-sm", styles.text)}>{labels.dragText}</p>
            )}
            <div className="flex items-center justify-center gap-3">
              <Button
                type="button"
                variant={styles.browseVariant}
                size="sm"
                disabled={disabled}
                className={cn(
                  tone === "primary" &&
                    "bg-muted text-muted-foreground hover:bg-muted/80",
                )}
                onClick={(event) => {
                  event.stopPropagation();
                  handleBrowseClick();
                }}
              >
                {labels.browseText}
              </Button>
              {secondaryAction && labels.orText && labels.secondaryActionText && (
                <>
                  <span className="text-sm text-muted-foreground">
                    {labels.orText}
                  </span>
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    disabled={disabled}
                    onClick={(event) => {
                      event.stopPropagation();
                      secondaryAction.onClick();
                    }}
                    className="gap-2"
                  >
                    {secondaryAction.icon}
                    {labels.secondaryActionText}
                  </Button>
                </>
              )}
            </div>
          </div>

          <div className={cn("space-y-1 text-xs", styles.helper)}>
            {acceptedTokens.length > 0 && <p>{acceptedTokens.join(", ")}</p>}
            {labels.maxSizeText && <p>{labels.maxSizeText}</p>}
          </div>

          <input
            ref={fileInputRef}
            type="file"
            className="hidden"
            onChange={handleInputChange}
            multiple={multiple}
            accept={resolvedAccept}
            disabled={disabled}
          />
        </div>
      </div>

      {renderPreview()}
    </div>
  );
}
