/**
 * Upload — 图片上传
 * @prop {string} [accept='image/*'] — 接受类型
 * @prop {boolean} [multiple=true] — 是否多选
 * @prop {number} limit — 数量上限
 * @prop {number} maxSize — 最大体积
 * @prop {number} minSize — 最小体积
 * @prop {boolean} [disabled=false] — 是否禁用
 * @prop {Array} value — 文件列表（受控）
 * @prop {Array} [defaultValue=null] — 默认列表
 * @prop {function} [onChange=null] — 列表变化
 * @prop {function} [onRemove=null] — 删除回调
 * @prop {function} [beforeUpload=null] — 上传前校验
 * @prop {function} [customRequest=null] — 自定义上传请求 ({ file, onProgress, onSuccess, onError }) => void；不传则文件加入即视为完成（done）
 * @prop {function} [onSuccess=null] — 成功回调
 * @prop {function} [onError=null] — 失败回调
 * @prop {function} [onProgress=null] — 进度回调
 */

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Button from './Button';
import Icon from './Icon';
import IconLoading from './IconLoading';

/* ── 布局容器 ── */
const WRAP = 'relative inline-flex flex-col';
const GALLERY = 'flex flex-wrap items-start gap-[8px]';

/* ── 图片卡片 ── */
const TILE_BASE = [
  'group relative size-[96px] shrink-0 overflow-hidden',
  'rounded-md',
  'border border-solid border-border-default',
  'bg-disabled',
].join(' ');

const TILE_IMAGE = 'size-full object-cover';

/* ── 添加卡片 ── */
const ADD_TILE = [
  'relative inline-flex size-[96px] shrink-0 items-center justify-center',
  'rounded-md',
  'border border-dashed border-border-default',
  'bg-fill text-foreground-secondary',
  'transition-colors tfds-motion-ui-fast',
  'hover:border-primary hover:bg-brand-50',
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
].join(' ');

const ADD_TILE_DISABLED = [
  'bg-disabled text-foreground-disabled',
  'border-border-default',
  'cursor-not-allowed pointer-events-none',
].join(' ');

/* ── 交互层 ── */
const REMOVE_BTN = [
  'absolute right-[5px] top-[5px] inline-flex size-[16px] items-center justify-center',
  'rounded-full border-none p-0',
  'bg-[rgba(0,0,0,0.55)] text-foreground-inverse',
  'shadow-[0_0_1px_rgba(0,0,0,0.32)]',
  'opacity-0 transition-opacity tfds-motion-ui-fast',
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
].join(' ');

const OVERLAY = 'absolute inset-0 bg-[rgba(0,0,0,0.3)]';

const ERROR_BADGE = [
  'absolute bottom-[5px] right-[5px] inline-flex size-[16px]',
  'items-center justify-center',
].join(' ');

function PlusIcon() {
  return (
    <svg viewBox="0 0 16 16" fill="none" className="size-[24px]" aria-hidden="true">
      <path d="M8 3V13M3 8H13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
    </svg>
  );
}

function CloseIcon() {
  return (
    <svg viewBox="0 0 16 16" fill="none" className="size-[12px]" aria-hidden="true">
      <path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
    </svg>
  );
}

function normalizeStatus(status) {
  if (status === 'uploading') return 'uploading';
  if (status === 'error' || status === 'uploadFail') return 'error';
  return 'done';
}

function normalizeList(rawList) {
  const list = Array.isArray(rawList) ? rawList : [];
  return list.map((item, idx) => ({
    ...item,
    uid: String(item?.uid ?? `${idx}-${item?.name || 'image'}`),
    status: normalizeStatus(item?.status),
    percent: typeof item?.percent === 'number' ? item.percent : (normalizeStatus(item?.status) === 'uploading' ? 40 : 100),
  }));
}

export const Upload = ({
  accept = 'image/*',
  multiple = true,
  limit,
  maxSize,
  minSize,
  disabled = false,
  value,
  defaultValue = null,
  onChange,
  onRemove,
  beforeUpload,
  customRequest,
  onSuccess,
  onError,
  onProgress,
  className = '',
  style,
  ...rest
}) => {
  const isControlled = value !== undefined;
  const [innerList, setInnerList] = useState(() => normalizeList(defaultValue));
  const currentList = isControlled ? normalizeList(value) : innerList;
  const listRef = useRef(currentList);

  const inputRef = useRef(null);
  const blobUrlsRef = useRef(new Set());

  useEffect(() => {
    listRef.current = currentList;
  }, [currentList]);

  const revokeBlobUrl = useCallback((url) => {
    if (typeof url !== 'string') return;
    if (!blobUrlsRef.current.has(url)) return;
    URL.revokeObjectURL(url);
    blobUrlsRef.current.delete(url);
  }, []);

  useEffect(() => {
    return () => {
      blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
      blobUrlsRef.current.clear();
    };
  }, []);

  useEffect(() => {
    if (!isControlled) setInnerList(normalizeList(defaultValue));
  }, [defaultValue, isControlled]);

  const updateList = useCallback((nextList) => {
    listRef.current = nextList;
    if (!isControlled) setInnerList(nextList);
    onChange?.(nextList);
  }, [isControlled, onChange]);

  const replaceByUid = useCallback((uid, updater) => {
    const base = listRef.current;
    const next = base.map((item) => (item.uid === uid ? updater(item) : item));
    updateList(next);
    return next;
  }, [updateList]);

  const removeItem = useCallback((target) => {
    if (disabled) return;
    revokeBlobUrl(target.url);
    const next = listRef.current.filter((item) => item.uid !== target.uid);
    updateList(next);
    onRemove?.(target, next);
  }, [disabled, onRemove, revokeBlobUrl, updateList]);

  const matchesAccept = useCallback((file) => {
    if (!accept) return true;
    const rules = String(accept).split(',').map((s) => s.trim()).filter(Boolean);
    if (rules.length === 0) return true;
    return rules.some((rule) => {
      if (rule === 'image/*') return file.type.startsWith('image/');
      if (rule.endsWith('/*')) return file.type.startsWith(rule.slice(0, -1));
      if (rule.startsWith('.')) return file.name.toLowerCase().endsWith(rule.toLowerCase());
      return file.type === rule;
    });
  }, [accept]);

  const validateFile = useCallback((file) => {
    if (!matchesAccept(file)) return { ok: false, reason: 'type' };
    if (typeof maxSize === 'number' && file.size > maxSize) return { ok: false, reason: 'maxSize' };
    if (typeof minSize === 'number' && file.size < minSize) return { ok: false, reason: 'minSize' };
    return { ok: true };
  }, [matchesAccept, maxSize, minSize]);

  const runBeforeUpload = useCallback(async (file) => {
    if (!beforeUpload) return true;
    try {
      const result = await beforeUpload(file);
      return result !== false;
    } catch {
      return false;
    }
  }, [beforeUpload]);

  const startUpload = useCallback((fileItem, rawFile) => {
    const uid = fileItem.uid;

    // 未提供 customRequest：组件不模拟网络请求，文件加入即视为完成
    if (typeof customRequest !== 'function') {
      const doneItem = { ...fileItem, status: 'done', percent: 100, errorReason: undefined };
      replaceByUid(uid, (prev) => ({
        ...prev,
        status: 'done',
        percent: 100,
        errorReason: undefined,
      }));
      onSuccess?.(doneItem, rawFile);
      return;
    }

    // 提供 customRequest：进入 uploading 态，由外部回调驱动状态机
    const uploadingItem = { ...fileItem, status: 'uploading', percent: 0 };
    replaceByUid(uid, (prev) => ({
      ...prev,
      status: 'uploading',
      percent: 0,
      errorReason: undefined,
    }));
    onProgress?.(0, uploadingItem, rawFile);

    customRequest({
      file: rawFile,
      onProgress: (percent) => {
        const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
        const next = { ...fileItem, status: 'uploading', percent: safePercent };
        replaceByUid(uid, (prev) => ({ ...prev, status: 'uploading', percent: safePercent }));
        onProgress?.(safePercent, next, rawFile);
      },
      onSuccess: (result) => {
        const doneItem = { ...fileItem, status: 'done', percent: 100, ...(result && typeof result === 'object' ? result : {}) };
        replaceByUid(uid, (prev) => ({ ...prev, ...doneItem, status: 'done', percent: 100, errorReason: undefined }));
        onSuccess?.(doneItem, rawFile);
      },
      onError: (reason) => {
        const errorItem = { ...fileItem, status: 'error', percent: 0, errorReason: reason || 'upload' };
        replaceByUid(uid, (prev) => ({ ...prev, status: 'error', percent: 0, errorReason: reason || 'upload' }));
        onError?.(reason || 'upload', errorItem, rawFile);
      },
    });
  }, [customRequest, onError, onProgress, onSuccess, replaceByUid]);

  const genUid = useCallback(
    () => `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    []
  );

  const addFiles = useCallback(async (rawFiles) => {
    if (disabled) return;

    const selectedFiles = Array.from(rawFiles || []);
    if (selectedFiles.length === 0) return;

    let working = [...listRef.current];
    const hasLimit = typeof limit === 'number' && Number.isFinite(limit);
    const maxCount = hasLimit ? limit : Infinity;

    for (const file of selectedFiles) {
      if (working.length >= maxCount) break;

      const uid = genUid();
      const previewUrl = URL.createObjectURL(file);
      blobUrlsRef.current.add(previewUrl);

      const baseItem = {
        uid,
        name: file.name,
        size: file.size,
        url: previewUrl,
        fileInstance: file,
        status: 'uploading',
        percent: 0,
      };

      const validation = validateFile(file);
      if (!validation.ok) {
        const errorItem = { ...baseItem, status: 'error', percent: 0, errorReason: validation.reason };
        working = [...working, errorItem];
        updateList(working);
        onError?.(validation.reason, errorItem, file);
        continue;
      }

      const allow = await runBeforeUpload(file);
      if (!allow) {
        const deniedItem = { ...baseItem, status: 'error', percent: 0, errorReason: 'beforeUpload' };
        working = [...working, deniedItem];
        updateList(working);
        onError?.('beforeUpload', deniedItem, file);
        continue;
      }

      working = [...working, baseItem];
      updateList(working);
      startUpload(baseItem, file);
    }
  }, [
    disabled,
    genUid,
    limit,
    onError,
    runBeforeUpload,
    startUpload,
    updateList,
    validateFile,
  ]);

  const openDialog = useCallback(() => {
    if (!disabled) inputRef.current?.click();
  }, [disabled]);

  const retryItem = useCallback(async (fileItem) => {
    if (disabled) return;
    const rawFile = fileItem?.fileInstance;

    if (rawFile) {
      const validation = validateFile(rawFile);
      if (!validation.ok) {
        replaceByUid(fileItem.uid, (prev) => ({ ...prev, status: 'error', percent: 0, errorReason: validation.reason }));
        onError?.(validation.reason, fileItem, rawFile);
        return;
      }

      const allow = await runBeforeUpload(rawFile);
      if (!allow) {
        replaceByUid(fileItem.uid, (prev) => ({ ...prev, status: 'error', percent: 0, errorReason: 'beforeUpload' }));
        onError?.('beforeUpload', fileItem, rawFile);
        return;
      }
    }

    startUpload({ ...fileItem, status: 'uploading', percent: 0 }, rawFile);
  }, [disabled, onError, replaceByUid, runBeforeUpload, startUpload, validateFile]);

  const handleInputChange = useCallback(async (e) => {
    const files = e.target.files;
    e.target.value = '';
    await addFiles(files);
  }, [addFiles]);

  const showAddTile = !disabled
    ? (typeof limit === 'number' ? currentList.length < limit : true)
    : (typeof limit === 'number' ? currentList.length < limit : currentList.length === 0);

  const normalizedList = useMemo(() => normalizeList(currentList), [currentList]);

  return (
    <div
      className={[`tfds-upload`, [WRAP, disabled ? 'opacity-60' : '', className].filter(Boolean).join(' ')].filter(Boolean).join(' ')}
      style={style}
      {...rest}
      data-tfds-component="Upload"
    >
      <input
        ref={inputRef}
        type="file"
        className="hidden"
        accept={accept}
        multiple={multiple}
        disabled={disabled}
        onChange={handleInputChange}
      />

      <div className={GALLERY}>
        {normalizedList.map((fileItem) => {
          const alwaysShowRemove = Boolean(fileItem.showRemove);
          const showUploading = fileItem.status === 'uploading';
          const showError = fileItem.status === 'error';

          return (
            <div key={fileItem.uid} className={TILE_BASE}>
              {fileItem.url ? (
                <img src={fileItem.url} alt={fileItem.name || 'upload image'} className={TILE_IMAGE} />
              ) : (
                <div className="flex size-full items-center justify-center text-foreground-disabled">
                  <PlusIcon />
                </div>
              )}

              {!disabled && (
                <button
                  type="button"
                  className={[
                    REMOVE_BTN,
                    alwaysShowRemove ? 'opacity-100' : 'group-hover:opacity-100 focus-visible:opacity-100',
                  ].join(' ')}
                  onClick={() => removeItem(fileItem)}
                  aria-label="删除图片"
                >
                  <CloseIcon />
                </button>
              )}

              {showUploading && (
                <>
                  <div className={OVERLAY} />
                  <div className="absolute inset-0 flex items-center justify-center">
                    <IconLoading size="md" aria-hidden="true" />
                  </div>
                </>
              )}

              {showError && (
                <>
                  <div className={OVERLAY} />
                  <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
                    <Button
                      variant="outline-black"
                      size="sm"
                      radius="rounded"
                      iconOnly
                      icon={<Icon name="refresh-cw-01-stroked" size="xs" />}
                      tooltip="重试上传"
                      onClick={(e) => {
                        e.stopPropagation();
                        retryItem(fileItem);
                      }}
                      disabled={disabled}
                      aria-label="重试上传"
                    />
                  </div>
                  <span className={ERROR_BADGE}>
                    <Icon
                      name="info-circle-stroked"
                      size="sm"
                      color="var(--color-danger)"
                      style={{ transform: 'rotate(180deg)' }}
                    />
                  </span>
                </>
              )}
            </div>
          );
        })}

        {showAddTile && (
          <button
            type="button"
            className={[ADD_TILE, disabled ? ADD_TILE_DISABLED : ''].join(' ')}
            onClick={openDialog}
            aria-label="添加图片"
          >
            <PlusIcon />
          </button>
        )}
      </div>
    </div>
  );
};

Upload.displayName = 'Upload';

export default Upload;
