import { useCallback, useRef, useState, type ChangeEvent, type RefObject } from 'react'; import { useTranslation } from 'react-i18next'; import { authFilesApi } from '@/services/api'; import { apiClient } from '@/services/api/client'; import { useNotificationStore } from '@/stores'; import type { AuthFileItem } from '@/types'; import { formatFileSize } from '@/utils/format'; import { MAX_AUTH_FILE_SIZE, getTypeLabel, isRuntimeOnlyAuthFile } from '@/features/authFiles/constants'; type DeleteAllOptions = { filter: string; onResetFilterToAll: () => void; }; export type UseAuthFilesDataResult = { files: AuthFileItem[]; loading: boolean; error: string; uploading: boolean; deleting: string | null; deletingAll: boolean; statusUpdating: Record; fileInputRef: RefObject; loadFiles: () => Promise; handleUploadClick: () => void; handleFileChange: (event: ChangeEvent) => Promise; handleDelete: (name: string) => void; handleDeleteAll: (options: DeleteAllOptions) => void; handleDownload: (name: string) => Promise; handleStatusToggle: (item: AuthFileItem, enabled: boolean) => Promise; }; export type UseAuthFilesDataOptions = { refreshKeyStats: () => Promise; }; export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFilesDataResult { const { refreshKeyStats } = options; const { t } = useTranslation(); const { showNotification, showConfirmation } = useNotificationStore(); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [uploading, setUploading] = useState(false); const [deleting, setDeleting] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [statusUpdating, setStatusUpdating] = useState>({}); const fileInputRef = useRef(null); const loadFiles = useCallback(async () => { setLoading(true); setError(''); try { const data = await authFilesApi.list(); setFiles(data?.files || []); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed'); setError(errorMessage); } finally { setLoading(false); } }, [t]); const handleUploadClick = useCallback(() => { fileInputRef.current?.click(); }, []); const handleFileChange = useCallback( async (event: ChangeEvent) => { const fileList = event.target.files; if (!fileList || fileList.length === 0) return; const filesToUpload = Array.from(fileList); const validFiles: File[] = []; const invalidFiles: string[] = []; const oversizedFiles: string[] = []; filesToUpload.forEach((file) => { if (!file.name.endsWith('.json')) { invalidFiles.push(file.name); return; } if (file.size > MAX_AUTH_FILE_SIZE) { oversizedFiles.push(file.name); return; } validFiles.push(file); }); if (invalidFiles.length > 0) { showNotification(t('auth_files.upload_error_json'), 'error'); } if (oversizedFiles.length > 0) { showNotification( t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }), 'error' ); } if (validFiles.length === 0) { event.target.value = ''; return; } setUploading(true); let successCount = 0; const failed: { name: string; message: string }[] = []; for (const file of validFiles) { try { await authFilesApi.upload(file); successCount++; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; failed.push({ name: file.name, message: errorMessage }); } } if (successCount > 0) { const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : ''; showNotification( `${t('auth_files.upload_success')}${suffix}`, failed.length ? 'warning' : 'success' ); await loadFiles(); await refreshKeyStats(); } if (failed.length > 0) { const details = failed.map((item) => `${item.name}: ${item.message}`).join('; '); showNotification(`${t('notification.upload_failed')}: ${details}`, 'error'); } setUploading(false); event.target.value = ''; }, [loadFiles, refreshKeyStats, showNotification, t] ); const handleDelete = useCallback( (name: string) => { showConfirmation({ title: t('auth_files.delete_title', { defaultValue: 'Delete File' }), message: `${t('auth_files.delete_confirm')} "${name}" ?`, variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { setDeleting(name); try { await authFilesApi.deleteFile(name); showNotification(t('auth_files.delete_success'), 'success'); setFiles((prev) => prev.filter((item) => item.name !== name)); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); } finally { setDeleting(null); } } }); }, [showConfirmation, showNotification, t] ); const handleDeleteAll = useCallback( (deleteAllOptions: DeleteAllOptions) => { const { filter, onResetFilterToAll } = deleteAllOptions; const isFiltered = filter !== 'all'; const typeLabel = isFiltered ? getTypeLabel(t, filter) : t('auth_files.filter_all'); const confirmMessage = isFiltered ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) : t('auth_files.delete_all_confirm'); showConfirmation({ title: t('auth_files.delete_all_title', { defaultValue: 'Delete All Files' }), message: confirmMessage, variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { setDeletingAll(true); try { if (!isFiltered) { await authFilesApi.deleteAll(); showNotification(t('auth_files.delete_all_success'), 'success'); setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); } else { const filesToDelete = files.filter( (f) => f.type === filter && !isRuntimeOnlyAuthFile(f) ); if (filesToDelete.length === 0) { showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); setDeletingAll(false); return; } let success = 0; let failed = 0; const deletedNames: string[] = []; for (const file of filesToDelete) { try { await authFilesApi.deleteFile(file.name); success++; deletedNames.push(file.name); } catch { failed++; } } setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); if (failed === 0) { showNotification( t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), 'success' ); } else { showNotification( t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), 'warning' ); } onResetFilterToAll(); } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); } finally { setDeletingAll(false); } } }); }, [files, showConfirmation, showNotification, t] ); const handleDownload = useCallback( async (name: string) => { try { const response = await apiClient.getRaw( `/auth-files/download?name=${encodeURIComponent(name)}`, { responseType: 'blob' } ); const blob = new Blob([response.data]); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); window.URL.revokeObjectURL(url); showNotification(t('auth_files.download_success'), 'success'); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error'); } }, [showNotification, t] ); const handleStatusToggle = useCallback( async (item: AuthFileItem, enabled: boolean) => { const name = item.name; const nextDisabled = !enabled; const previousDisabled = item.disabled === true; setStatusUpdating((prev) => ({ ...prev, [name]: true })); setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: nextDisabled } : f))); try { const res = await authFilesApi.setStatus(name, nextDisabled); setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: res.disabled } : f)) ); showNotification( enabled ? t('auth_files.status_enabled_success', { name }) : t('auth_files.status_disabled_success', { name }), 'success' ); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f)) ); showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error'); } finally { setStatusUpdating((prev) => { if (!prev[name]) return prev; const next = { ...prev }; delete next[name]; return next; }); } }, [showNotification, t] ); return { files, loading, error, uploading, deleting, deletingAll, statusUpdating, fileInputRef, loadFiles, handleUploadClick, handleFileChange, handleDelete, handleDeleteAll, handleDownload, handleStatusToggle }; }