import type { JSX } from 'preact';
import { useCallback, useMemo, useState } from 'preact/hooks';

import { cn } from './cn';
import { Button } from './Button';
import { FileDropzone } from './FileDropzone';
import { Input } from './Input';
import { hasConfiguredUploadPicker, openConfiguredUploadPicker, resolveUploadedFileUrl, resolveUploadSelectionUrl } from '../../core/images';
import type { AsukaUploadModuleConfig, AsukaUploadStrategy } from '../../types/options';

export type MediaInputMode = 'image' | 'icon' | 'svg' | 'model';

/**
 * MediaInput (core)
 * - URL input + (optional) upload via FileDropzone
 * - Designed to be used inside <Form.Item /> (Input-like binding via onInput/onChange)
 * - Extendable: modes control accept/preview sizing/labels.
 */
export type MediaInputProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onInput' | 'onChange'> & {
  /** Current value (URL or data URL) */
  value?: string;
  /** Input-like handlers expected by <Form.Item /> */
  onInput?: (e: any) => void;
  onChange?: (e: any) => void;

  /** Preset behavior (defaults, accept, preview size). Default: 'image' */
  mode?: MediaInputMode;
  placeholder?: string;
  /** Overrides the default accept computed from mode */
  accept?: string;
  /** When true, shows the dropzone below the URL input. Default: true */
  allowUpload?: boolean;
  /** When true, shows a preview square when the value looks like an image/SVG. Default: true */
  showPreview?: boolean;
  /** Preview size in px. Default depends on mode. */
  previewSize?: number;
  /** Optional hint shown under the dropzone */
  uploadHint?: string;
  /** One-off upload policy override. Defaults to `moduleConfig.upload`. */
  upload?: AsukaUploadModuleConfig;
  /** Fallback strategy when no upload strategy/endpoint/handler is configured. Defaults to `base64` for persisted form values. */
  uploadDefaultStrategy?: AsukaUploadStrategy;
  /** Optional custom source name passed to upload.handler. */
  uploadSource?: string;
  /** Called immediately when local files are selected/dropped, before upload/url resolution. */
  onFilesSelected?: (files: File[], context: { mode: MediaInputMode; source: string }) => void | Promise<void>;
  /** Called when file upload/read fails. */
  onUploadError?: (error: unknown) => void;
  /** Mark the underlying URL input as required. */
  required?: boolean;
  /** Uses a denser URL input and a horizontal file picker for editor side panels. */
  compact?: boolean;
};

function emitValue(next: string, handlers: { onInput?: (e: any) => void; onChange?: (e: any) => void }) {
  const event = { currentTarget: { value: next } };
  // Programmatic uploads/picker selections should behave like one user input.
  // Calling both handlers makes ModelField/ImageField patch form state twice,
  // which is especially expensive for large GLB data URLs and 3D part detection.
  if (handlers.onInput) handlers.onInput(event);
  else handlers.onChange?.(event);
}

function looksLikePreviewable(value: string) {
  const v = (value ?? '').trim();
  if (!v) return false;
  if (v.startsWith('data:image/')) return true;
  // URL heuristic for common image extensions (incl. svg)
  return /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/i.test(v);
}

function isLargeStoredMediaValue(value: string, mode: MediaInputMode): boolean {
  const v = String(value ?? '').trim();
  if (!v) return false;
  // A GLB saved as a data URL can be tens of MB. Putting that string directly
  // in a controlled <input value="..."> makes every form render and every
  // variation edit progressively slower. Keep the real value in form state, but
  // render a compact, read-only label for local embedded model/blob values.
  return mode === 'model' && (/^data:/i.test(v) || /^blob:/i.test(v) || v.length > 4096);
}

function formatStoredMediaValue(value: string, mode: MediaInputMode): string {
  const v = String(value ?? '').trim();
  if (!isLargeStoredMediaValue(v, mode)) return v;
  if (/^blob:/i.test(v)) return '[local GLB/glTF model selected]';
  const mime = v.match(/^data:([^;,]+)/i)?.[1] ?? 'model';
  const base64 = v.match(/^data:[^,]*,(.*)$/i)?.[1] ?? '';
  const estimatedBytes = base64 ? Math.floor((base64.length * 3) / 4) : v.length;
  const mb = estimatedBytes / (1024 * 1024);
  const size = mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.max(1, Math.round(estimatedBytes / 1024))} KB`;
  return `[embedded ${mime} · ${size}]`;
}

function defaultsForMode(mode: MediaInputMode) {
  switch (mode) {
    case 'icon':
      return {
        accept: 'image/*',
        placeholder: 'https://…',
        previewSize: 32,
        uploadHint: 'The file will be stored using the configured upload strategy',
      };
    case 'svg':
      return {
        accept: 'image/svg+xml',
        placeholder: 'https://…',
        previewSize: 36,
        uploadHint: 'The SVG will be stored using the configured upload strategy',
      };
    case 'model':
      return {
        accept: '.glb,.gltf,model/gltf-binary,model/gltf+json,application/octet-stream,application/json',
        placeholder: '/models/product.glb',
        previewSize: 36,
        uploadHint: 'Upload or select a GLB/glTF model. Use a remote upload strategy for production URLs.',
      };
    case 'image':
    default:
      return {
        accept: 'image/*',
        placeholder: 'https://…',
        previewSize: 36,
        uploadHint: 'The file will be stored using the configured upload strategy',
      };
  }
}

export function MediaInput({ value, onInput, onChange, mode = 'image', placeholder, accept, allowUpload = true, showPreview = true, previewSize, uploadHint, upload, uploadDefaultStrategy = 'base64', uploadSource, onFilesSelected, onUploadError, required, compact = false, className, ...rest }: MediaInputProps) {
  const defaults = useMemo(() => defaultsForMode(mode), [mode]);
  const src = useMemo(() => (typeof value === 'string' ? value.trim() : ''), [value]);

  const resolvedPlaceholder = placeholder ?? defaults.placeholder;
  const resolvedAccept = accept ?? defaults.accept;
  const resolvedPreviewSize = previewSize ?? (compact ? Math.min(defaults.previewSize, 28) : defaults.previewSize);
  const resolvedUploadHint = uploadHint ?? defaults.uploadHint;

  const canPreview = showPreview && looksLikePreviewable(src);
  const compactStoredValue = isLargeStoredMediaValue(src, mode);
  const inputValue = compactStoredValue ? formatStoredMediaValue(src, mode) : src;
  const [uploading, setUploading] = useState(false);
  const [uploadError, setUploadError] = useState<string | null>(null);

  const onFiles = useCallback(
    (files: File[]) => {
      const file = files?.[0];
      if (!file) return;

      try {
        void onFilesSelected?.([file], { mode, source: uploadSource ?? `media-input:${mode}` });
      } catch {}

      setUploading(true);
      setUploadError(null);

      void resolveUploadedFileUrl(file, {
        upload,
        defaultStrategy: uploadDefaultStrategy,
        source: uploadSource ?? `media-input:${mode}`,
        mode,
      })
        .then((next) => {
          emitValue(next, { onInput, onChange });
        })
        .catch((error) => {
          const message = error instanceof Error ? error.message : 'Upload failed.';
          setUploadError(message);
          onUploadError?.(error);
        })
        .finally(() => {
          setUploading(false);
        });
    },
    [mode, onChange, onFilesSelected, onInput, onUploadError, upload, uploadDefaultStrategy, uploadSource],
  );

  const hasUploadPicker = hasConfiguredUploadPicker(upload);
  const browseWithConfiguredPicker = useCallback(async () => {
    if (!hasConfiguredUploadPicker(upload)) return false;

    setUploading(true);
    setUploadError(null);

    try {
      const selections = await openConfiguredUploadPicker(upload, {
        accept: resolvedAccept,
        multiple: false,
        source: uploadSource ?? `media-input:${mode}`,
        mode,
        kind: 'media-input',
      });
      if (!selections) return false;
      const first = selections[0];
      if (!first) return true;
      const next = await resolveUploadSelectionUrl(first, {
        upload,
        defaultStrategy: uploadDefaultStrategy,
        source: uploadSource ?? `media-input:${mode}`,
        mode,
      });
      emitValue(next, { onInput, onChange });
      return true;
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Upload failed.';
      setUploadError(message);
      onUploadError?.(error);
      return true;
    } finally {
      setUploading(false);
    }
  }, [mode, onChange, onInput, onUploadError, resolvedAccept, upload, uploadDefaultStrategy, uploadSource]);

  return (
    <div className={cn('ask-flex ask-flex-col', compact ? 'ask-gap-1.5' : 'ask-gap-2', className)} aria-required={required ? true : undefined} {...rest}>
      <div className="ask-flex ask-items-center ask-gap-2">
        {canPreview ? (
          <div className={cn('ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-overflow-hidden', compact ? 'ask-rounded-md' : 'ask-rounded-[10px]')} style={{ width: resolvedPreviewSize, height: resolvedPreviewSize }}>
            {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
            {/* @ts-ignore */}
            <img src={src} alt="preview" className="ask-h-full ask-w-full ask-object-cover" />
          </div>
        ) : null}

        <Input value={inputValue} required={required} aria-required={required ? true : undefined} placeholder={resolvedPlaceholder} readOnly={compactStoredValue ? true : undefined} title={compactStoredValue ? 'The full model value is stored internally. Clear it to replace it with another URL or file.' : undefined} compact={compact} onInput={compactStoredValue ? undefined : (e: any) => onInput?.(e)} onChange={compactStoredValue ? undefined : (e: any) => onChange?.(e)} />

        {src ? (
          <Button type="button" size="sm" className={cn('ask-bg-surfaceAlt hover:ask-bg-surface', compact && 'ask-h-8 ask-px-2.5 ask-text-[11px]')} onClick={() => emitValue('', { onInput, onChange })}>
            Clear
          </Button>
        ) : null}
      </div>

      {compactStoredValue ? <div className="ask-text-[10px] ask-leading-4 ask-text-textMuted">Embedded model kept in memory. Use Clear to replace it; configure an upload handler for production URLs.</div> : null}

      {allowUpload ? <FileDropzone accept={resolvedAccept} label={mode === 'icon' ? 'Upload icon' : mode === 'svg' ? 'Upload SVG' : mode === 'model' ? 'Upload GLB/glTF' : 'Upload image'} hint={uploading ? 'Uploading…' : resolvedUploadHint} uploadContext="media-input" compact={compact} onBrowse={hasUploadPicker ? browseWithConfiguredPicker : undefined} onFiles={onFiles} /> : null}

      {uploadError ? <div className="ask-text-[10px] ask-text-danger">{uploadError}</div> : null}
    </div>
  );
}
