import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';

import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/constants/app_sections.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/filter_bar.dart';
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
import 'package:superport_v2/widgets/components/superport_dialog.dart';
import 'package:superport_v2/widgets/components/feedback.dart';
import 'package:superport_v2/widgets/components/form_field.dart';
import 'package:superport_v2/widgets/components/empty_state.dart';
import 'package:superport_v2/features/inventory/shared/widgets/product_autocomplete_field.dart';
import 'package:superport_v2/features/inventory/shared/widgets/employee_autocomplete_field.dart';
import 'package:superport_v2/features/inventory/shared/widgets/customer_multi_select_field.dart';
import 'package:superport_v2/features/inventory/shared/widgets/warehouse_select_field.dart';
import 'package:superport_v2/features/approvals/request/presentation/controllers/approval_request_controller.dart';
import 'package:superport_v2/features/approvals/request/presentation/utils/approval_form_initializer.dart';
import 'package:superport_v2/features/approvals/request/presentation/widgets/approval_step_configurator.dart';
import 'package:superport_v2/features/approvals/request/presentation/widgets/approval_template_picker.dart';
import 'package:superport_v2/features/approvals/domain/usecases/get_approval_draft_use_case.dart';
import 'package:superport_v2/features/approvals/domain/usecases/list_approval_drafts_use_case.dart';
import 'package:superport_v2/features/approvals/domain/usecases/save_approval_draft_use_case.dart';
import 'package:superport_v2/core/config/environment.dart';
import 'package:superport_v2/core/network/failure.dart';
import 'package:superport_v2/core/permissions/permission_manager.dart';
import 'package:superport_v2/core/permissions/permission_resources.dart';
import 'package:superport_v2/features/inventory/rental/presentation/controllers/rental_controller.dart';
import 'package:superport_v2/features/inventory/rental/presentation/models/rental_record.dart';
import 'package:superport_v2/features/inventory/rental/presentation/specs/rental_table_spec.dart';
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
import 'package:superport_v2/features/inventory/transactions/presentation/services/transaction_detail_sync_service.dart';
import 'package:superport_v2/features/inventory/transactions/presentation/widgets/transaction_detail_dialog.dart';
import 'package:superport_v2/features/auth/application/auth_service.dart';
import '../../../lookups/domain/entities/lookup_item.dart';
import '../../../lookups/domain/repositories/inventory_lookup_repository.dart';
import '../widgets/rental_detail_view.dart';

const String _rentalTransactionTypeRent = '대여';
const String _rentalTransactionTypeReturn = '반납';

class _RentalDetailSections {
  static const lines = 'lines';
}

/// 대여/반납 목록과 등록 모달을 관리하는 페이지.
class RentalPage extends StatefulWidget {
  const RentalPage({super.key, required this.routeUri});

  final Uri routeUri;

  @override
  State<RentalPage> createState() => _RentalPageState();
}

/// 대여 화면의 상태를 관리하고 사용자 동작을 처리한다.
class _RentalPageState extends State<RentalPage> {
  final TextEditingController _searchController = TextEditingController();
  final DateFormat _dateFormatter = DateFormat('yyyy-MM-dd');
  final NumberFormat _currencyFormatter = NumberFormat.currency(
    locale: 'ko_KR',
    symbol: '₩',
    decimalDigits: 0,
  );
  final TransactionDetailSyncService _detailSyncService =
      const TransactionDetailSyncService();

  String _query = '';
  String _pendingQuery = '';
  int? _appliedWarehouseId;
  int? _pendingWarehouseId;
  String? _appliedWarehouseLabel;
  String? _pendingWarehouseLabel;
  String? _appliedStatus;
  String? _pendingStatus;
  String? _appliedRentalType;
  String? _pendingRentalType;
  DateTimeRange? _appliedDateRange;
  DateTimeRange? _pendingDateRange;
  DateTimeRange? _appliedReturnRange;
  DateTimeRange? _pendingReturnRange;
  List<RentalRecord> _records = [];
  RentalRecord? _selectedRecord;

  PaginatedResult<StockTransaction>? _result;
  bool _isLoading = false;
  String? _errorMessage;
  Set<int> _processingTransactionIds = {};

  String? _routeSelectedNumber;
  String? _pendingDetailNumber;
  bool _suppressNextRouteSelection = false;

  late List<String> _statusOptions;
  final Map<String, LookupItem> _statusLookup = {};
  LookupItem? _rentTransactionType;
  LookupItem? _returnTransactionType;

  int _currentPage = 1;
  int _pageSize = RentalTableSpec.pageSizeOptions.first;
  _RentalSortField _sortField = _RentalSortField.processedAt;
  bool _sortAscending = false;
  RentalController? _controller;
  Set<String> _appliedIncludes = {...RentalTableSpec.defaultIncludeOptions};
  Set<String> _pendingIncludes = {...RentalTableSpec.defaultIncludeOptions};
  late final ShadSelectController<String> _includeController;

  bool get _transitionsEnabled =>
      Environment.flag('FEATURE_STOCK_TRANSITIONS_ENABLED', defaultValue: true);

  @override
  void initState() {
    super.initState();
    _includeController = ShadSelectController<String>(
      initialValue: _pendingIncludes.toSet(),
    );
    _statusOptions = List<String>.from(RentalTableSpec.fallbackStatusOptions);
    _controller = _createController();
    _controller?.addListener(_handleControllerChanged);
    _applyRouteParameters(widget.routeUri, initialize: true);
    _initializeController();
  }

  RentalController? _createController() {
    final getIt = GetIt.I;
    if (!getIt.isRegistered<StockTransactionRepository>() ||
        !getIt.isRegistered<TransactionLineRepository>() ||
        !getIt.isRegistered<TransactionCustomerRepository>() ||
        !getIt.isRegistered<InventoryLookupRepository>()) {
      return null;
    }
    return RentalController(
      transactionRepository: getIt<StockTransactionRepository>(),
      lineRepository: getIt<TransactionLineRepository>(),
      customerRepository: getIt<TransactionCustomerRepository>(),
      lookupRepository: getIt<InventoryLookupRepository>(),
      fallbackStatusOptions: RentalTableSpec.fallbackStatusOptions,
      rentTransactionKeywords: RentalTableSpec.rentTransactionKeywords,
      returnTransactionKeywords: RentalTableSpec.returnTransactionKeywords,
      saveDraftUseCase: getIt.isRegistered<SaveApprovalDraftUseCase>()
          ? getIt<SaveApprovalDraftUseCase>()
          : null,
      getDraftUseCase: getIt.isRegistered<GetApprovalDraftUseCase>()
          ? getIt<GetApprovalDraftUseCase>()
          : null,
      listDraftsUseCase: getIt.isRegistered<ListApprovalDraftsUseCase>()
          ? getIt<ListApprovalDraftsUseCase>()
          : null,
    );
  }

  void _initializeController() {
    final controller = _controller;
    if (controller == null) {
      return;
    }
    Future.microtask(() async {
      await controller.loadStatusOptions();
      final requester = _resolveCurrentWriter();
      if (requester != null && _canRestoreApprovalDrafts) {
        await controller.loadApprovalDraftFromServer(requesterId: requester.id);
      }
      final hasTypes = await controller.resolveTransactionTypes();
      if (!mounted) {
        return;
      }
      if (hasTypes) {
        await _fetchTransactions(page: _currentPage);
      }
    });
  }

  bool get _canRestoreApprovalDrafts {
    final getIt = GetIt.I;
    if (!getIt.isRegistered<PermissionManager>()) {
      return false;
    }
    return getIt<PermissionManager>().can(
      PermissionResources.approvals,
      PermissionAction.view,
    );
  }

  void _handleControllerChanged() {
    if (!mounted) {
      return;
    }
    final controller = _controller;
    if (controller == null) {
      return;
    }
    final statusSource = controller.statusOptions.isEmpty
        ? controller.fallbackStatusOptions
        : controller.statusOptions;
    final resolvedStatusOptions = List<String>.from(statusSource);
    final resolvedLookup = Map<String, LookupItem>.from(
      controller.statusLookup,
    );

    var pendingStatus = _pendingStatus;
    if (pendingStatus != null &&
        !resolvedStatusOptions.contains(pendingStatus)) {
      pendingStatus = null;
    }
    var appliedStatus = _appliedStatus;
    if (appliedStatus != null &&
        !resolvedStatusOptions.contains(appliedStatus)) {
      appliedStatus = null;
    }

    setState(() {
      _statusOptions = resolvedStatusOptions.isEmpty
          ? List<String>.from(RentalTableSpec.fallbackStatusOptions)
          : resolvedStatusOptions;
      _statusLookup
        ..clear()
        ..addAll(resolvedLookup);
      _pendingStatus = pendingStatus;
      _appliedStatus = appliedStatus;
      _rentTransactionType = controller.rentTransactionType;
      _returnTransactionType = controller.returnTransactionType;
      _result = controller.result;
      _records = controller.records.toList();
      _isLoading = controller.isLoading;
      _errorMessage = controller.errorMessage;
      _processingTransactionIds = Set<int>.from(
        controller.processingTransactionIds,
      );
      _refreshSelection();
    });
    final detailNumber = _pendingDetailNumber;
    if (detailNumber != null) {
      _pendingDetailNumber = null;
      RentalRecord? matched;
      for (final record in _records) {
        if (record.transactionNumber == detailNumber) {
          matched = record;
          break;
        }
      }
      if (matched != null) {
        final target = matched;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (!mounted) {
            return;
          }
          _showDetailDialog(target);
        });
      }
    }
  }

  InventoryEmployeeSuggestion? _resolveCurrentWriter() {
    final getIt = GetIt.I;
    if (!getIt.isRegistered<AuthService>()) {
      return null;
    }
    final session = getIt<AuthService>().session;
    final user = session?.user;
    if (user == null) {
      return null;
    }
    final employeeNo = (user.employeeNo ?? '').trim().isEmpty
        ? user.id.toString()
        : user.employeeNo!.trim();
    return InventoryEmployeeSuggestion(
      id: user.id,
      employeeNo: employeeNo,
      name: user.name,
    );
  }

  @override
  void didUpdateWidget(covariant RentalPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.routeUri != widget.routeUri) {
      _applyRouteParameters(widget.routeUri);
    }
  }

  @override
  void dispose() {
    _controller?.removeListener(_handleControllerChanged);
    _controller?.dispose();
    _searchController.dispose();
    _includeController.dispose();
    super.dispose();
  }

  Future<void> _fetchTransactions({int? page}) async {
    final controller = _controller;
    if (controller == null) {
      return;
    }

    final targetPage = page ?? _currentPage;
    final includes = _appliedIncludes.isEmpty
        ? RentalTableSpec.defaultIncludeOptions.toList(growable: false)
        : _appliedIncludes.toList(growable: false);
    final statusItem = _appliedStatus == null
        ? null
        : _statusLookup[_appliedStatus!];

    int? transactionTypeId;
    switch (_appliedRentalType) {
      case _rentalTransactionTypeRent:
        transactionTypeId = controller.rentTransactionType?.id;
        break;
      case _rentalTransactionTypeReturn:
        transactionTypeId = controller.returnTransactionType?.id;
        break;
      default:
        transactionTypeId = null;
    }

    setState(() {
      _currentPage = targetPage;
    });

    final filter = StockTransactionListFilter(
      page: targetPage,
      pageSize: _pageSize,
      query: _query.isEmpty ? null : _query,
      transactionTypeId: transactionTypeId,
      transactionStatusId: statusItem != null && statusItem.id > 0
          ? statusItem.id
          : null,
      from: _appliedDateRange?.start,
      to: _appliedDateRange?.end,
      include: includes,
    );

    await controller.fetchTransactions(filter: filter);
  }

  @override
  Widget build(BuildContext context) {
    final theme = ShadTheme.of(context);
    final filtered = _filteredRecords;
    final totalItems = _result?.total ?? filtered.length;
    final totalPages = _calculateTotalPages(totalItems);
    final currentPage = totalPages <= 1
        ? 1
        : (_currentPage < 1
              ? 1
              : (_currentPage > totalPages ? totalPages : _currentPage));
    final visibleRecords = filtered;

    return AppLayout(
      title: '대여 관리',
      subtitle: '대여/반납 구분, 반납 예정일, 고객사 현황을 확인합니다.',
      breadcrumbs: const [
        AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
        AppBreadcrumbItem(label: '입·출고', path: '/inventory/rental'),
        AppBreadcrumbItem(label: '대여'),
      ],
      actions: [
        PermissionGate(
          resource: PermissionResources.stockTransactions,
          action: PermissionAction.create,
          child: ShadButton(
            leading: const Icon(lucide.LucideIcons.plus, size: 16),
            onPressed: _handleCreate,
            child: const Text('대여 등록'),
          ),
        ),
        PermissionGate(
          resource: PermissionResources.stockTransactions,
          action: PermissionAction.edit,
          child: ShadButton.outline(
            leading: const Icon(lucide.LucideIcons.pencil, size: 16),
            onPressed: _selectedRecord == null
                ? null
                : () => _handleEdit(_selectedRecord!),
            child: const Text('선택 항목 수정'),
          ),
        ),
      ],
      toolbar: FilterBar(
        actionConfig: FilterBarActionConfig(
          onApply: _applyFilters,
          onReset: _resetFilters,
          hasPendingChanges: _hasDirtyFilters,
          hasActiveFilters: _hasAppliedFilters,
        ),
        children: [
          SizedBox(
            width: 260,
            child: ShadInput(
              controller: _searchController,
              placeholder: const Text(RentalTableSpec.searchPlaceholder),
              leading: const Icon(lucide.LucideIcons.search, size: 16),
              onChanged: (_) {
                setState(() {
                  _pendingQuery = _searchController.text;
                });
              },
              onSubmitted: (_) => _applyFilters(),
            ),
          ),
          SizedBox(
            width: 220,
            child: SuperportDateRangePickerButton(
              value: _pendingDateRange,
              dateFormat: _dateFormatter,
              onChanged: (range) => setState(() => _pendingDateRange = range),
              firstDate: RentalTableSpec.dateRangeFirstDate,
              lastDate: RentalTableSpec.dateRangeLastDate,
            ),
          ),
          SizedBox(
            width: 220,
            child: SuperportDateRangePickerButton(
              value: _pendingReturnRange,
              dateFormat: _dateFormatter,
              onChanged: (range) => setState(() => _pendingReturnRange = range),
              firstDate: RentalTableSpec.dateRangeFirstDate,
              lastDate: RentalTableSpec.dateRangeLastDate,
              placeholder: '반납 예정일 범위',
            ),
          ),
          SizedBox(
            width: 200,
            child: InventoryWarehouseSelectField(
              key: ValueKey(_pendingWarehouseId ?? 'all'),
              initialWarehouseId: _pendingWarehouseId,
              includeAllOption: true,
              allLabel: RentalTableSpec.allWarehouseLabel,
              placeholder: const Text(RentalTableSpec.allWarehouseLabel),
              onChanged: (option) {
                setState(() {
                  _pendingWarehouseId = option?.id;
                  _pendingWarehouseLabel = option?.name;
                });
              },
            ),
          ),
          SizedBox(
            width: 200,
            child: ShadSelect<String?>(
              key: ValueKey(_pendingStatus ?? 'all'),
              initialValue: _pendingStatus,
              selectedOptionBuilder: (_, value) =>
                  Text(value ?? RentalTableSpec.allStatusLabel),
              onChanged: (value) => setState(() => _pendingStatus = value),
              options: [
                ShadOption<String?>(
                  value: null,
                  child: Text(RentalTableSpec.allStatusLabel),
                ),
                for (final option in _statusOptions)
                  ShadOption<String?>(value: option, child: Text(option)),
              ],
            ),
          ),
          SizedBox(
            width: 200,
            child: ShadSelect<String?>(
              key: ValueKey(_pendingRentalType ?? 'all'),
              initialValue: _pendingRentalType,
              selectedOptionBuilder: (_, value) =>
                  Text(value ?? RentalTableSpec.allRentalTypeLabel),
              onChanged: (value) => setState(() => _pendingRentalType = value),
              options: [
                ShadOption<String?>(
                  value: null,
                  child: Text(RentalTableSpec.allRentalTypeLabel),
                ),
                for (final option in RentalTableSpec.rentalTypes)
                  ShadOption<String?>(value: option, child: Text(option)),
              ],
            ),
          ),
          SizedBox(
            width: 180,
            child: ShadSelect<_RentalSortField>(
              key: ValueKey(_sortField),
              initialValue: _sortField,
              selectedOptionBuilder: (_, value) => Text(_sortLabel(value)),
              onChanged: (value) {
                if (value == null) {
                  return;
                }
                setState(() {
                  _sortField = value;
                  _currentPage = 1;
                });
                _updateRoute(page: 1);
              },
              options: [
                for (final option in _RentalSortField.values)
                  ShadOption(value: option, child: Text(_sortLabel(option))),
              ],
            ),
          ),
          SizedBox(
            width: 220,
            child: ShadSelect<String>.multiple(
              key: ValueKey(_pendingIncludes.hashCode),
              controller: _includeController,
              initialValues: _pendingIncludes,
              placeholder: const Text(RentalTableSpec.includeEmptyLabel),
              selectedOptionsBuilder: (context, values) {
                if (values.isEmpty) {
                  return const Text(RentalTableSpec.includeEmptyLabel);
                }
                return Wrap(
                  spacing: 6,
                  runSpacing: 6,
                  children: [
                    for (final value in values)
                      ShadBadge(
                        child: Padding(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 4,
                          ),
                          child: Text(RentalTableSpec.includeLabel(value)),
                        ),
                      ),
                  ],
                );
              },
              onChanged: (values) {
                setState(() {
                  _pendingIncludes = values;
                });
              },
              options: [
                for (final option in RentalTableSpec.defaultIncludeOptions)
                  ShadOption(
                    value: option,
                    child: Text(RentalTableSpec.includeLabel(option)),
                  ),
              ],
            ),
          ),
          ShadButton.outline(
            onPressed: () {
              setState(() {
                _sortAscending = !_sortAscending;
                _currentPage = 1;
              });
              _updateRoute(page: 1);
            },
            leading: const Icon(lucide.LucideIcons.arrowUpDown, size: 16),
            child: Text(_sortAscending ? '오름차순' : '내림차순'),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ShadCard(
            title: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('대여 내역', style: theme.textTheme.h3),
                Text('$totalItems건', style: theme.textTheme.muted),
              ],
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SizedBox(
                  height: 420,
                  child: _isLoading && filtered.isEmpty
                      ? const Center(child: CircularProgressIndicator())
                      : filtered.isEmpty
                      ? SuperportEmptyState(
                          title: _errorMessage != null
                              ? '데이터를 불러오지 못했습니다.'
                              : '대여 데이터가 없습니다.',
                          description: _errorMessage ?? '검색 조건을 조정해 다시 시도하세요.',
                        )
                      : () {
                          final baseOffset = _rowNumberOffset(
                            visibleRecords.length,
                          );
                          return ShadTable.list(
                            header: RentalTableSpec.headers
                                .map(
                                  (header) =>
                                      ShadTableCell.header(child: Text(header)),
                                )
                                .toList(),
                            children: [
                              for (var i = 0; i < visibleRecords.length; i++)
                                _buildRecordRow(
                                  visibleRecords[i],
                                  baseOffset + i + 1,
                                ).map(
                                  (value) => ShadTableCell(
                                    child: Text(
                                      value,
                                      overflow: TextOverflow.ellipsis,
                                    ),
                                  ),
                                ),
                            ],
                            columnSpanExtent: (index) =>
                                const FixedTableSpanExtent(
                                  RentalTableSpec.columnSpanWidth,
                                ),
                            rowSpanExtent: (index) =>
                                const FixedTableSpanExtent(
                                  RentalTableSpec.rowSpanHeight,
                                ),
                            onRowTap: (rowIndex) {
                              if (rowIndex <= 0 ||
                                  rowIndex > visibleRecords.length) {
                                return;
                              }
                              final record = visibleRecords[rowIndex - 1];
                              _selectRecord(record, openDetail: true);
                            },
                          );
                        }(),
                ),
                if (filtered.isNotEmpty) ...[
                  const SizedBox(height: 12),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      SizedBox(
                        width: 170,
                        child: ShadSelect<int>(
                          key: ValueKey(_pageSize),
                          initialValue: _pageSize,
                          selectedOptionBuilder: (_, value) =>
                              Text('$value개 / 페이지'),
                          onChanged: (value) {
                            if (value == null) {
                              return;
                            }
                            setState(() {
                              _pageSize = value;
                              _currentPage = 1;
                            });
                            _updateRoute(page: 1, pageSize: value);
                            _fetchTransactions(page: 1);
                          },
                          options: [
                            for (final option
                                in RentalTableSpec.pageSizeOptions)
                              ShadOption(
                                value: option,
                                child: Text('$option개 / 페이지'),
                              ),
                          ],
                        ),
                      ),
                      Row(
                        children: [
                          Text(
                            '$totalItems건 · 페이지 $currentPage / $totalPages',
                            style: theme.textTheme.small,
                          ),
                          const SizedBox(width: 12),
                          ShadButton.ghost(
                            size: ShadButtonSize.sm,
                            onPressed: currentPage <= 1
                                ? null
                                : () => _goToPage(1),
                            child: const Icon(
                              lucide.LucideIcons.chevronsLeft,
                              size: 16,
                            ),
                          ),
                          const SizedBox(width: 8),
                          ShadButton.ghost(
                            size: ShadButtonSize.sm,
                            onPressed: currentPage <= 1
                                ? null
                                : () => _goToPage(currentPage - 1),
                            child: const Icon(
                              lucide.LucideIcons.chevronLeft,
                              size: 16,
                            ),
                          ),
                          const SizedBox(width: 8),
                          ShadButton.ghost(
                            size: ShadButtonSize.sm,
                            onPressed: currentPage >= totalPages
                                ? null
                                : () => _goToPage(currentPage + 1),
                            child: const Icon(
                              lucide.LucideIcons.chevronRight,
                              size: 16,
                            ),
                          ),
                          const SizedBox(width: 8),
                          ShadButton.ghost(
                            size: ShadButtonSize.sm,
                            onPressed: currentPage >= totalPages
                                ? null
                                : () => _goToPage(totalPages),
                            child: const Icon(
                              lucide.LucideIcons.chevronsRight,
                              size: 16,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }

  List<RentalRecord> get _filteredRecords {
    final query = _query.trim().toLowerCase();
    final range = _appliedDateRange;
    final returnRange = _appliedReturnRange;

    final records = _records.where((record) {
      final matchesQuery =
          query.isEmpty ||
          record.number.toLowerCase().contains(query) ||
          record.transactionNumber.toLowerCase().contains(query) ||
          record.writer.toLowerCase().contains(query) ||
          record.customers.any(
            (customer) =>
                customer.name.toLowerCase().contains(query) ||
                customer.code.toLowerCase().contains(query),
          ) ||
          record.items.any(
            (item) => item.product.toLowerCase().contains(query),
          );
      final matchesRange =
          range == null ||
          (!record.processedAt.isBefore(range.start) &&
              !record.processedAt.isAfter(range.end));
      final matchesReturnRange = () {
        final due = record.returnDueDate;
        if (returnRange == null) {
          return true;
        }
        if (due == null) {
          return false;
        }
        return !due.isBefore(returnRange.start) &&
            !due.isAfter(returnRange.end);
      }();
      final matchesWarehouse = () {
        if (_appliedWarehouseId != null) {
          return record.warehouseId == _appliedWarehouseId;
        }
        if (_appliedWarehouseLabel != null &&
            _appliedWarehouseLabel!.isNotEmpty) {
          return record.warehouse == _appliedWarehouseLabel;
        }
        return true;
      }();
      final matchesStatus =
          _appliedStatus == null || record.status == _appliedStatus;
      final matchesRentalType =
          _appliedRentalType == null || record.rentalType == _appliedRentalType;
      return matchesQuery &&
          matchesRange &&
          matchesReturnRange &&
          matchesWarehouse &&
          matchesStatus &&
          matchesRentalType;
    }).toList();

    records.sort((a, b) {
      final compare = switch (_sortField) {
        _RentalSortField.processedAt => a.processedAt.compareTo(b.processedAt),
        _RentalSortField.returnDueDate => _compareDateTimeNullable(
          a.returnDueDate,
          b.returnDueDate,
        ),
        _RentalSortField.warehouse => a.warehouse.compareTo(b.warehouse),
        _RentalSortField.status => a.status.compareTo(b.status),
        _RentalSortField.rentalType => a.rentalType.compareTo(b.rentalType),
        _RentalSortField.customerCount => a.customerCount.compareTo(
          b.customerCount,
        ),
        _RentalSortField.itemCount => a.itemCount.compareTo(b.itemCount),
        _RentalSortField.totalQuantity => a.totalQuantity.compareTo(
          b.totalQuantity,
        ),
      };
      return _sortAscending ? compare : -compare;
    });

    return records;
  }

  void _selectRecord(RentalRecord? record, {bool openDetail = false}) {
    if (!mounted) return;
    setState(() {
      _selectedRecord = record;
    });
    final selectedNumber = record?.transactionNumber;
    _routeSelectedNumber = selectedNumber;
    _updateRoute(selected: selectedNumber);
    if (openDetail && record != null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (!mounted) {
          return;
        }
        _showDetailDialog(record);
      });
    }
  }

  List<String> _buildRecordRow(RentalRecord record, int displayIndex) {
    return [
      displayIndex.toString(),
      _dateFormatter.format(record.processedAt),
      record.warehouse,
      record.rentalType,
      record.transactionNumber,
      record.status,
      record.returnDueDate == null
          ? '-'
          : _dateFormatter.format(record.returnDueDate!),
      record.customerCount.toString(),
      record.itemCount.toString(),
      record.totalQuantity.toString(),
      record.remark.isEmpty ? '-' : record.remark,
    ];
  }

  int _rowNumberOffset(int currentCount) {
    final page = _result?.page ?? _currentPage;
    final pageSize = _result?.pageSize ?? _pageSize;
    final safePage = page > 0 ? page : 1;
    final safePageSize = pageSize > 0
        ? pageSize
        : (currentCount > 0 ? currentCount : 1);
    return (safePage - 1) * safePageSize;
  }

  Future<void> _showDetailDialog(RentalRecord record) async {
    await showInventoryTransactionDetailDialog<void>(
      context: context,
      title: '대여 상세',
      description: '대여 고객사와 라인 품목을 확인하세요.',
      summary: _buildRentalDetailSummary(record),
      summaryBadges: _buildRentalDetailBadges(record),
      metadata: _buildRentalDetailMetadata(record),
      sections: [
        SuperportDetailDialogSection(
          id: _RentalDetailSections.lines,
          label: '라인 품목',
          icon: lucide.LucideIcons.listChecks,
          builder: (_) => RentalDetailView(
            record: record,
            currencyFormatter: _currencyFormatter,
            transitionsEnabled: _transitionsEnabled,
          ),
        ),
      ],
      initialSectionId: _RentalDetailSections.lines,
      actions: _buildDetailActions(record),
    );
  }

  Widget _buildRentalDetailSummary(RentalRecord record) {
    final theme = ShadTheme.of(context);
    final processedAt = _dateFormatter.format(record.processedAt);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(record.transactionNumber, style: theme.textTheme.h4),
        const SizedBox(height: 4),
        Text(
          '$processedAt · ${record.rentalType}',
          style: theme.textTheme.muted,
        ),
      ],
    );
  }

  List<Widget> _buildRentalDetailBadges(RentalRecord record) {
    return [
      ShadBadge(child: Text(record.status)),
      ShadBadge.outline(child: Text(record.transactionType)),
    ];
  }

  List<SuperportDetailMetadata> _buildRentalDetailMetadata(
    RentalRecord record,
  ) {
    return [
      SuperportDetailMetadata.text(
        label: '트랜잭션번호',
        value: record.transactionNumber,
      ),
      SuperportDetailMetadata.text(
        label: '처리일자',
        value: _dateFormatter.format(record.processedAt),
      ),
      SuperportDetailMetadata.text(
        label: '트랜잭션 유형',
        value: record.transactionType,
      ),
      SuperportDetailMetadata.text(label: '대여 구분', value: record.rentalType),
      SuperportDetailMetadata.text(label: '상태', value: record.status),
      SuperportDetailMetadata.text(label: '작성자', value: record.writer),
      SuperportDetailMetadata.text(label: '창고', value: record.warehouse),
      SuperportDetailMetadata.text(
        label: '창고 코드',
        value: _dashIfEmpty(record.warehouseCode),
      ),
      SuperportDetailMetadata.text(
        label: '창고 우편번호',
        value: _dashIfEmpty(record.warehouseZipcode),
      ),
      SuperportDetailMetadata.text(
        label: '창고 주소',
        value: _dashIfEmpty(record.warehouseAddress),
      ),
      SuperportDetailMetadata.text(
        label: '고객 수',
        value: '${record.customerCount}',
      ),
      SuperportDetailMetadata.text(label: '품목 수', value: '${record.itemCount}'),
      SuperportDetailMetadata.text(
        label: '총 수량',
        value: '${record.totalQuantity}',
      ),
      SuperportDetailMetadata.text(
        label: '총 금액',
        value: _currencyFormatter.format(record.totalAmount),
      ),
      SuperportDetailMetadata.text(
        label: '반납 예정일',
        value: record.returnDueDate == null
            ? '-'
            : _dateFormatter.format(record.returnDueDate!),
      ),
      SuperportDetailMetadata.text(
        label: '비고',
        value: _dashIfEmpty(record.remark),
      ),
    ];
  }

  String _dashIfEmpty(String? value) {
    if (value == null) {
      return '-';
    }
    final trimmed = value.trim();
    return trimmed.isEmpty ? '-' : trimmed;
  }

  List<Widget> _buildDetailActions(RentalRecord record) {
    final isProcessing = _isProcessing(record.id);
    final actions = <Widget>[];

    if (_canSubmit(record)) {
      actions.add(
        ShadButton.outline(
          onPressed: isProcessing ? null : () => _submitRecord(record),
          child: const Text('반납 요청'),
        ),
      );
    }
    if (_canApprove(record)) {
      actions.add(
        ShadButton.outline(
          onPressed: isProcessing ? null : () => _approveRecord(record),
          child: const Text('승인'),
        ),
      );
    }
    if (_canReject(record)) {
      actions.add(
        ShadButton.outline(
          onPressed: isProcessing ? null : () => _rejectRecord(record),
          child: const Text('반려'),
        ),
      );
    }
    if (_canCancel(record)) {
      actions.add(
        ShadButton.outline(
          onPressed: isProcessing ? null : () => _cancelRecord(record),
          child: const Text('취소'),
        ),
      );
    }
    if (_canComplete(record)) {
      actions.add(
        ShadButton.outline(
          onPressed: isProcessing ? null : () => _completeRecord(record),
          child: const Text('대여 완료'),
        ),
      );
    }
    actions.add(
      ShadButton.outline(
        leading: const Icon(lucide.LucideIcons.pencil, size: 16),
        onPressed: isProcessing ? null : () => _openEditFromDetail(record),
        child: const Text('수정'),
      ),
    );

    return actions;
  }

  void _openEditFromDetail(RentalRecord record) {
    final navigator = Navigator.of(context, rootNavigator: true);
    navigator.pop();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      if (!mounted) {
        return;
      }
      await _handleEdit(record, reopenOnCancel: true);
    });
  }

  Future<void> _handleCreate() async {
    final record = await _showRentalFormDialog();
    if (record != null) {
      _selectRecord(record, openDetail: true);
    }
  }

  Future<void> _handleEdit(
    RentalRecord record, {
    bool reopenOnCancel = false,
  }) async {
    final updated = await _showRentalFormDialog(initial: record);
    if (updated != null) {
      _selectRecord(updated, openDetail: true);
    } else if (reopenOnCancel) {
      _selectRecord(record, openDetail: true);
    }
  }

  bool _isProcessing(int? id) {
    if (id == null) {
      return false;
    }
    return _processingTransactionIds.contains(id);
  }

  bool _canSubmit(RentalRecord record) {
    if (!_transitionsEnabled) {
      return false;
    }
    final status = record.status.trim();
    return status.contains('대여') && !status.contains('반납');
  }

  bool _canComplete(RentalRecord record) {
    if (!_transitionsEnabled) {
      return false;
    }
    final status = record.status.trim();
    return status.contains('반납') || status.contains('대기');
  }

  bool _canApprove(RentalRecord record) {
    if (!_transitionsEnabled) {
      return false;
    }
    final normalized = record.status.replaceAll(RegExp(r'\s+'), '');
    return normalized.contains('반납대기');
  }

  bool _canReject(RentalRecord record) {
    if (!_transitionsEnabled) {
      return false;
    }
    final normalized = record.status.replaceAll(RegExp(r'\s+'), '');
    return normalized.contains('반납대기');
  }

  bool _canCancel(RentalRecord record) {
    if (!_transitionsEnabled) {
      return false;
    }
    final normalized = record.status.replaceAll(RegExp(r'\s+'), '');
    return normalized.contains('대여중') || normalized.contains('반납대기');
  }

  /// 실패 객체에서 사용자 메시지를 추출한다.
  String _failureMessage(Object error, String fallbackMessage) {
    final failure = Failure.from(error);
    final description = failure.describe();
    if (description.isEmpty) {
      return fallbackMessage;
    }
    return description;
  }

  Future<void> _submitRecord(RentalRecord record) async {
    final controller = _controller;
    final id = record.id;
    if (controller == null || id == null) {
      if (mounted) {
        SuperportToast.error(context, '반납 요청 대상을 찾을 수 없습니다.');
      }
      return;
    }

    try {
      await controller.submitTransaction(id);
      if (!mounted) {
        return;
      }
      SuperportToast.success(context, '반납 요청을 완료했습니다.');
    } catch (error) {
      if (!mounted) {
        return;
      }
      SuperportToast.error(
        context,
        _failureMessage(error, '반납 요청 처리에 실패했습니다. 다시 시도하세요.'),
      );
    }
  }

  Future<void> _completeRecord(RentalRecord record) async {
    final controller = _controller;
    final id = record.id;
    if (controller == null || id == null) {
      if (mounted) {
        SuperportToast.error(context, '완료 처리할 대여 데이터를 찾지 못했습니다.');
      }
      return;
    }

    try {
      await controller.completeTransaction(id);
      if (!mounted) {
        return;
      }
      SuperportToast.success(context, '대여 완료 처리되었습니다.');
    } catch (error) {
      if (!mounted) {
        return;
      }
      SuperportToast.error(
        context,
        _failureMessage(error, '대여 완료 처리에 실패했습니다. 잠시 후 다시 시도하세요.'),
      );
    }
  }

  Future<void> _approveRecord(RentalRecord record) async {
    final controller = _controller;
    final id = record.id;
    if (controller == null || id == null) {
      if (mounted) {
        SuperportToast.error(context, '승인할 대여 데이터를 찾을 수 없습니다.');
      }
      return;
    }

    try {
      await controller.approveTransaction(id);
      if (!mounted) {
        return;
      }
      SuperportToast.success(context, '반납 승인이 완료되었습니다.');
    } catch (error) {
      if (!mounted) {
        return;
      }
      SuperportToast.error(
        context,
        _failureMessage(error, '반납 승인 처리에 실패했습니다. 잠시 후 다시 시도하세요.'),
      );
    }
  }

  Future<void> _rejectRecord(RentalRecord record) async {
    final controller = _controller;
    final id = record.id;
    if (controller == null || id == null) {
      if (mounted) {
        SuperportToast.error(context, '반려할 대여 데이터를 찾을 수 없습니다.');
      }
      return;
    }

    try {
      await controller.rejectTransaction(id);
      if (!mounted) {
        return;
      }
      SuperportToast.success(context, '반납이 반려되었습니다.');
    } catch (error) {
      if (!mounted) {
        return;
      }
      SuperportToast.error(
        context,
        _failureMessage(error, '반납 반려 처리에 실패했습니다. 다시 시도하세요.'),
      );
    }
  }

  Future<void> _cancelRecord(RentalRecord record) async {
    final controller = _controller;
    final id = record.id;
    if (controller == null || id == null) {
      if (mounted) {
        SuperportToast.error(context, '취소할 대여 데이터를 찾지 못했습니다.');
      }
      return;
    }

    try {
      await controller.cancelTransaction(id);
      if (!mounted) {
        return;
      }
      SuperportToast.success(context, '대여 요청이 취소되었습니다.');
    } catch (error) {
      if (!mounted) {
        return;
      }
      SuperportToast.error(
        context,
        _failureMessage(error, '대여 취소 처리에 실패했습니다. 잠시 후 다시 시도하세요.'),
      );
    }
  }

  void _applyFilters() {
    setState(() {
      _query = _pendingQuery.trim();
      _appliedDateRange = _pendingDateRange;
      _appliedReturnRange = _pendingReturnRange;
      _appliedWarehouseId = _pendingWarehouseId;
      _appliedWarehouseLabel = _pendingWarehouseLabel;
      _appliedStatus = _pendingStatus;
      _appliedRentalType = _pendingRentalType;
      _appliedIncludes = {..._pendingIncludes};
      _currentPage = 1;
      _refreshSelection();
    });
    _updateRoute(page: 1);
    _fetchTransactions(page: 1);
  }

  void _resetFilters() {
    setState(() {
      _searchController.clear();
      _pendingQuery = '';
      _query = '';
      _pendingDateRange = null;
      _appliedDateRange = null;
      _pendingReturnRange = null;
      _appliedReturnRange = null;
      _pendingWarehouseId = null;
      _appliedWarehouseId = null;
      _pendingWarehouseLabel = null;
      _appliedWarehouseLabel = null;
      _pendingStatus = null;
      _appliedStatus = null;
      _pendingRentalType = null;
      _appliedRentalType = null;
      _currentPage = 1;
      _pageSize = RentalTableSpec.pageSizeOptions.first;
      _sortField = _RentalSortField.processedAt;
      _sortAscending = false;
      _pendingIncludes = {...RentalTableSpec.defaultIncludeOptions};
      _appliedIncludes = {...RentalTableSpec.defaultIncludeOptions};
      _includeController
        ..value.clear()
        ..value.addAll(_pendingIncludes);
      _refreshSelection();
    });
    _updateRoute(page: 1, pageSize: RentalTableSpec.pageSizeOptions.first);
    _fetchTransactions(page: 1);
  }

  bool get _hasAppliedFilters =>
      _query.isNotEmpty ||
      _appliedDateRange != null ||
      _appliedReturnRange != null ||
      _appliedWarehouseId != null ||
      (_appliedWarehouseLabel != null && _appliedWarehouseLabel!.isNotEmpty) ||
      _appliedStatus != null ||
      _appliedRentalType != null ||
      !_setEquals(
        _appliedIncludes,
        RentalTableSpec.defaultIncludeOptions.toSet(),
      );

  bool get _hasDirtyFilters =>
      _pendingQuery.trim() != _query ||
      !_isSameRange(_pendingDateRange, _appliedDateRange) ||
      !_isSameRange(_pendingReturnRange, _appliedReturnRange) ||
      _pendingWarehouseId != _appliedWarehouseId ||
      _pendingWarehouseLabel != _appliedWarehouseLabel ||
      _pendingStatus != _appliedStatus ||
      _pendingRentalType != _appliedRentalType ||
      !_setEquals(_pendingIncludes, _appliedIncludes);

  bool _isSameRange(DateTimeRange? a, DateTimeRange? b) {
    if (identical(a, b)) {
      return true;
    }
    if (a == null || b == null) {
      return a == b;
    }
    return a.start == b.start && a.end == b.end;
  }

  void _applyRouteParameters(Uri uri, {bool initialize = false}) {
    final params = uri.queryParameters;
    final query = params['q'] ?? '';
    final warehouseIdParam = int.tryParse(params['warehouse_id'] ?? '');
    final warehouseParam = params['warehouse'];
    final statusParam = params['status'];
    final rentalTypeParam = params['rental_type'];
    final dateRange = _parseDateRange(params['start_date'], params['end_date']);
    final returnRange = _parseDateRange(
      params['return_start'],
      params['return_end'],
    );
    final includesParam = params['include'];
    final resolvedSortField =
        _sortFieldFromParam(params['sort']) ?? _RentalSortField.processedAt;
    final resolvedSortAscending =
        (params['order'] ?? '').toLowerCase() == 'asc';
    final pageSizeParam = int.tryParse(params['page_size'] ?? '');
    final pageParam = int.tryParse(params['page'] ?? '');
    final selectedParam = params['selected']?.trim();
    final selectedNumber = selectedParam == null || selectedParam.isEmpty
        ? null
        : selectedParam;

    final warehouseId = warehouseIdParam;
    final warehouseLabel = warehouseParam != null && warehouseParam.isNotEmpty
        ? warehouseParam
        : null;
    final status = _statusOptions.contains(statusParam) ? statusParam : null;
    final rentalType = RentalTableSpec.rentalTypes.contains(rentalTypeParam)
        ? rentalTypeParam
        : null;
    final includes = includesParam == null || includesParam.isEmpty
        ? {...RentalTableSpec.defaultIncludeOptions}
        : includesParam
              .split(',')
              .map((token) => token.trim())
              .where(
                (token) =>
                    RentalTableSpec.defaultIncludeOptions.contains(token),
              )
              .toSet();
    final pageSize =
        (pageSizeParam != null &&
            RentalTableSpec.pageSizeOptions.contains(pageSizeParam))
        ? pageSizeParam
        : RentalTableSpec.pageSizeOptions.first;
    final page = (pageParam != null && pageParam > 0) ? pageParam : 1;

    void assign() {
      _pendingQuery = query;
      _query = query;
      _searchController.text = query;
      _pendingWarehouseId = warehouseId;
      _appliedWarehouseId = warehouseId;
      _pendingWarehouseLabel = warehouseLabel;
      _appliedWarehouseLabel = warehouseLabel;
      _pendingStatus = status;
      _appliedStatus = status;
      _pendingRentalType = rentalType;
      _appliedRentalType = rentalType;
      _pendingDateRange = dateRange;
      _appliedDateRange = dateRange;
      _pendingReturnRange = returnRange;
      _appliedReturnRange = returnRange;
      _pendingIncludes = includes.isEmpty
          ? {...RentalTableSpec.defaultIncludeOptions}
          : includes;
      _appliedIncludes = {..._pendingIncludes};
      _includeController
        ..value.clear()
        ..value.addAll(_pendingIncludes);
      _sortField = resolvedSortField;
      _sortAscending = resolvedSortAscending;
      _pageSize = pageSize;
      _currentPage = page;
      _routeSelectedNumber = selectedNumber;
      if (_suppressNextRouteSelection) {
        _pendingDetailNumber = null;
      } else {
        _pendingDetailNumber = selectedNumber;
      }
      _suppressNextRouteSelection = false;
      _refreshSelection();
    }

    if (initialize) {
      assign();
      return;
    }

    setState(assign);
    _fetchTransactions(page: page);
  }

  void _goToPage(int page) {
    final totalItems = _result?.total ?? _filteredRecords.length;
    final totalPages = _calculateTotalPages(totalItems);
    final int target = page < 1 ? 1 : (page > totalPages ? totalPages : page);
    if (target == _currentPage) {
      return;
    }
    setState(() {
      _currentPage = target;
    });
    _updateRoute(page: target);
    _fetchTransactions(page: target);
  }

  void _refreshSelection() {
    final filtered = _filteredRecords;
    if (filtered.isEmpty) {
      _selectedRecord = null;
      return;
    }

    final selectedNumber = _routeSelectedNumber;
    if (selectedNumber != null) {
      for (final record in filtered) {
        if (record.transactionNumber == selectedNumber) {
          _selectedRecord = record;
          return;
        }
      }
    }

    final current = _selectedRecord;
    if (current != null) {
      RentalRecord? matched;
      if (current.id != null) {
        matched = filtered.firstWhere(
          (element) => element.id == current.id,
          orElse: () => filtered.first,
        );
      } else {
        matched = filtered.firstWhere(
          (element) => element.transactionNumber == current.transactionNumber,
          orElse: () => filtered.first,
        );
      }
      _selectedRecord = matched;
      return;
    }

    _selectedRecord = filtered.first;
  }

  void _updateRoute({int? page, int? pageSize, String? selected}) {
    if (!mounted) return;
    final targetPage = page ?? _currentPage;
    final targetPageSize = pageSize ?? _pageSize;

    final params = <String, String>{};
    if (_query.isNotEmpty) {
      params['q'] = _query;
    }
    if (_appliedWarehouseId != null) {
      params['warehouse_id'] = _appliedWarehouseId.toString();
      if (_appliedWarehouseLabel != null &&
          _appliedWarehouseLabel!.isNotEmpty) {
        params['warehouse'] = _appliedWarehouseLabel!;
      }
    } else if (_appliedWarehouseLabel != null &&
        _appliedWarehouseLabel!.isNotEmpty) {
      params['warehouse'] = _appliedWarehouseLabel!;
    }
    if (_appliedStatus != null && _appliedStatus!.isNotEmpty) {
      params['status'] = _appliedStatus!;
    }
    if (_appliedRentalType != null && _appliedRentalType!.isNotEmpty) {
      params['rental_type'] = _appliedRentalType!;
    }
    final dateRange = _appliedDateRange;
    if (dateRange != null) {
      params['start_date'] = _formatDateParam(dateRange.start);
      params['end_date'] = _formatDateParam(dateRange.end);
    }
    final returnRange = _appliedReturnRange;
    if (returnRange != null) {
      params['return_start'] = _formatDateParam(returnRange.start);
      params['return_end'] = _formatDateParam(returnRange.end);
    }
    if (!_setEquals(
      _appliedIncludes,
      RentalTableSpec.defaultIncludeOptions.toSet(),
    )) {
      params['include'] = _appliedIncludes.join(',');
    }
    final sortParam = _encodeSortField(_sortField);
    if (sortParam != null) {
      params['sort'] = sortParam;
    }
    if (_sortAscending) {
      params['order'] = 'asc';
    }
    if (targetPage > 1) {
      params['page'] = targetPage.toString();
    }
    if (targetPageSize != RentalTableSpec.pageSizeOptions.first) {
      params['page_size'] = targetPageSize.toString();
    }
    final selectedNumber = selected ?? _selectedRecord?.transactionNumber;
    if (selectedNumber != null && selectedNumber.isNotEmpty) {
      params['selected'] = selectedNumber;
    }

    final uri = Uri(
      path: widget.routeUri.path,
      queryParameters: params.isEmpty ? null : params,
    );
    final newLocation = uri.toString();
    if (newLocation == widget.routeUri.toString()) {
      return;
    }
    final router = GoRouter.maybeOf(context);
    if (router == null) {
      return;
    }
    _suppressNextRouteSelection = true;
    router.go(newLocation);
  }

  DateTime? _parseDate(String? value) {
    if (value == null || value.isEmpty) {
      return null;
    }
    return DateTime.tryParse(value);
  }

  DateTimeRange? _parseDateRange(String? start, String? end) {
    final startDate = _parseDate(start);
    final endDate = _parseDate(end);
    if (startDate == null || endDate == null) {
      return null;
    }
    if (endDate.isBefore(startDate)) {
      return null;
    }
    return DateTimeRange(start: startDate, end: endDate);
  }

  String _formatDateParam(DateTime date) => _dateFormatter.format(date);

  _RentalSortField? _sortFieldFromParam(String? value) {
    switch (value) {
      case 'return_due_date':
        return _RentalSortField.returnDueDate;
      case 'warehouse':
        return _RentalSortField.warehouse;
      case 'status':
        return _RentalSortField.status;
      case 'rental_type':
        return _RentalSortField.rentalType;
      case 'customer_count':
        return _RentalSortField.customerCount;
      case 'item_count':
        return _RentalSortField.itemCount;
      case 'total_quantity':
        return _RentalSortField.totalQuantity;
      case 'processed_at':
        return _RentalSortField.processedAt;
      default:
        return null;
    }
  }

  String? _encodeSortField(_RentalSortField field) {
    switch (field) {
      case _RentalSortField.processedAt:
        return null;
      case _RentalSortField.returnDueDate:
        return 'return_due_date';
      case _RentalSortField.warehouse:
        return 'warehouse';
      case _RentalSortField.status:
        return 'status';
      case _RentalSortField.rentalType:
        return 'rental_type';
      case _RentalSortField.customerCount:
        return 'customer_count';
      case _RentalSortField.itemCount:
        return 'item_count';
      case _RentalSortField.totalQuantity:
        return 'total_quantity';
    }
  }

  int _calculateTotalPages(int totalCount) {
    if (totalCount <= 0) {
      return 1;
    }
    return ((totalCount - 1) ~/ _pageSize) + 1;
  }

  bool _setEquals(Set<String> a, Set<String> b) {
    if (identical(a, b)) return true;
    if (a.length != b.length) return false;
    for (final value in a) {
      if (!b.contains(value)) return false;
    }
    return true;
  }

  String _sortLabel(_RentalSortField field) {
    return switch (field) {
      _RentalSortField.processedAt => '처리일자',
      _RentalSortField.returnDueDate => '반납예정일',
      _RentalSortField.warehouse => '창고',
      _RentalSortField.status => '상태',
      _RentalSortField.rentalType => '대여구분',
      _RentalSortField.customerCount => '고객수',
      _RentalSortField.itemCount => '품목수',
      _RentalSortField.totalQuantity => '총수량',
    };
  }

  int _compareDateTimeNullable(DateTime? a, DateTime? b) {
    if (a == null && b == null) {
      return 0;
    }
    if (a == null) {
      return 1;
    }
    if (b == null) {
      return -1;
    }
    return a.compareTo(b);
  }

  String _transactionTypeForRental(String rentalType) {
    final lookup = rentalType == RentalTableSpec.rentalTypes.last
        ? _returnTransactionType
        : _rentTransactionType;
    if (lookup != null) {
      return lookup.name;
    }
    return rentalType == RentalTableSpec.rentalTypes.last
        ? _rentalTransactionTypeReturn
        : _rentalTransactionTypeRent;
  }

  /// 대여 등록/수정 모달을 띄워 사용자가 입력한 레코드를 반환한다.
  Future<RentalRecord?> _showRentalFormDialog({RentalRecord? initial}) async {
    final processedAt = ValueNotifier<DateTime>(
      initial?.processedAt ?? DateTime.now(),
    );
    final returnDue = ValueNotifier<DateTime?>(initial?.returnDueDate);
    final initialWarehouse = initial?.raw?.warehouse;
    final int? initialWarehouseId =
        initialWarehouse?.id ?? initial?.warehouseId;
    final String? initialWarehouseName =
        initialWarehouse?.name ?? initial?.warehouse;
    final String? initialWarehouseCode = initialWarehouse?.code;
    InventoryWarehouseOption? warehouseSelection;
    if (initialWarehouseId != null && initialWarehouseName != null) {
      warehouseSelection = InventoryWarehouseOption(
        id: initialWarehouseId,
        code: initialWarehouseCode ?? initialWarehouseName,
        name: initialWarehouseName,
      );
    }
    final statusValue = ValueNotifier<String>(
      initial?.status ?? _statusOptions.first,
    );
    final rentalTypeValue = ValueNotifier<String>(
      initial?.rentalType ?? RentalTableSpec.rentalTypes.first,
    );
    InventoryEmployeeSuggestion? writerSelection;
    final initialWriter = initial?.raw?.createdBy;
    if (initialWriter != null) {
      writerSelection = InventoryEmployeeSuggestion(
        id: initialWriter.id,
        employeeNo: initialWriter.employeeNo,
        name: initialWriter.name,
      );
    } else if (initial?.writerId != null &&
        (initial?.writer ?? '').isNotEmpty) {
      writerSelection = InventoryEmployeeSuggestion(
        id: initial!.writerId!,
        employeeNo: initial.writer,
        name: initial.writer,
      );
    }
    String writerLabel(InventoryEmployeeSuggestion? suggestion) {
      if (suggestion == null) {
        return '';
      }
      return '${suggestion.name} (${suggestion.employeeNo})';
    }

    final approvalController = ApprovalRequestController();
    final defaultRequester = () {
      final writer = writerSelection;
      if (writer == null) {
        return null;
      }
      return ApprovalRequestParticipant(
        id: writer.id,
        name: writer.name,
        employeeNo: writer.employeeNo,
      );
    }();
    await ApprovalFormInitializer.populate(
      controller: approvalController,
      existingApproval: initial?.raw?.approval,
      draft: _controller?.approvalDraft,
      defaultRequester: defaultRequester,
    );
    if (!mounted) {
      return null;
    }

    final writerController = TextEditingController(
      text: writerLabel(writerSelection),
    );
    final remarkController = TextEditingController(text: initial?.remark ?? '');
    final initialCustomers = initial?.customers ?? const <RentalCustomer>[];
    final Set<int> initialCustomerIds = {
      for (final customer in initialCustomers) customer.customerId,
    };
    final Map<int, _RentalSelectedCustomerDraft> customerDraftMap = {
      for (final customer in initialCustomers)
        customer.customerId: _RentalSelectedCustomerDraft(
          option: InventoryCustomerOption(
            id: customer.customerId,
            code: customer.code,
            name: customer.name,
            industry: '-',
            region: '-',
          ),
          linkId: customer.id,
          note: customer.note,
        ),
    };
    List<_RentalSelectedCustomerDraft> customerSelection = customerDraftMap
        .values
        .toList(growable: false);
    final transactionTypeController = TextEditingController(
      text: _transactionTypeForRental(rentalTypeValue.value),
    );
    final assignedTransactionNo = initial?.transactionNumber;
    final assignedApprovalNo = initial?.raw?.approval?.approvalNo;
    final approvalNoteController = TextEditingController();

    final drafts =
        initial?.items
            .map((item) => _RentalLineItemDraft.fromItem(item))
            .toList()
            .cast<_RentalLineItemDraft>() ??
        [_RentalLineItemDraft.empty()];

    RentalRecord? result;
    String? writerError;
    String? customerError;
    String? warehouseError;
    String? statusError;
    String? headerNotice;
    StateSetter? refreshForm;
    StateSetter? refreshCancelAction;
    StateSetter? refreshSaveAction;
    var isSaving = false;

    final lineErrors = {
      for (final draft in drafts) draft: _RentalLineItemErrors.empty(),
    };

    final navigator = Navigator.of(context, rootNavigator: true);

    void updateSaving(bool next) {
      isSaving = next;
      refreshForm?.call(() {});
      refreshCancelAction?.call(() {});
      refreshSaveAction?.call(() {});
    }

    Future<void> handleSubmit() async {
      if (isSaving) {
        return;
      }

      final validation = _validateRentalForm(
        writerController: writerController,
        writerSelection: writerSelection,
        requireWriterSelection: initial == null,
        warehouseSelection: warehouseSelection,
        statusValue: statusValue.value,
        selectedCustomers: customerSelection
            .map((draft) => draft.option)
            .toList(growable: false),
        drafts: drafts,
        lineErrors: lineErrors,
      );

      writerError = validation.writerError;
      customerError = validation.customerError;
      warehouseError = validation.warehouseError;
      statusError = validation.statusError;
      headerNotice = validation.headerNotice;
      refreshForm?.call(() {});

      if (!validation.isValid) {
        SuperportToast.error(context, '입력 오류를 확인하고 다시 시도하세요.');
        return;
      }
      final controller = _controller;
      if (controller == null) {
        SuperportToast.error(context, '대여 컨트롤러를 찾을 수 없습니다.');
        return;
      }

      Future<RentalRecord?> resolveUpdatedRecord(int? id) async {
        if (id == null) {
          return null;
        }
        final detail = await controller.fetchTransactionDetail(id);
        if (detail != null) {
          return detail;
        }
        for (final record in controller.records) {
          if (record.id == id) {
            return record;
          }
        }
        return null;
      }

      final selectedLookup =
          rentalTypeValue.value == RentalTableSpec.rentalTypes.last
          ? (_returnTransactionType ?? controller.returnTransactionType)
          : (_rentTransactionType ?? controller.rentTransactionType);
      if (selectedLookup == null) {
        SuperportToast.error(context, '대여/반납 트랜잭션 유형 정보를 불러오지 못했습니다.');
        return;
      }

      final statusItem = _statusLookup[statusValue.value];
      if (statusItem == null) {
        SuperportToast.error(context, '선택한 상태 정보를 확인할 수 없습니다.');
        return;
      }

      final warehouseId = warehouseSelection?.id ?? initialWarehouseId;
      if (warehouseId == null) {
        SuperportToast.error(context, '창고 정보를 확인할 수 없습니다.');
        return;
      }

      final createdById = writerSelection?.id ?? initial?.writerId;
      if (initial == null && createdById == null) {
        SuperportToast.error(context, '작성자 정보를 확인할 수 없습니다.');
        return;
      }

      final remarkText = remarkController.text.trim();
      final remarkValue = remarkText.isEmpty ? null : remarkText;
      final approvalNoteValue = approvalNoteController.text.trim();
      final transactionId = initial?.id;
      final initialRecord = initial;

      final lineDrafts = <TransactionLineDraft>[];
      for (var index = 0; index < drafts.length; index++) {
        final draft = drafts[index];
        final productId = draft.productId ?? draft.suggestion?.id;
        if (productId == null) {
          continue;
        }
        final quantity = int.tryParse(draft.quantity.text.trim()) ?? 0;
        final unitPrice = _parseCurrency(draft.price.text);
        final noteText = draft.remark.text.trim();
        lineDrafts.add(
          TransactionLineDraft(
            id: draft.lineId,
            lineNo: index + 1,
            productId: productId,
            quantity: quantity,
            unitPrice: unitPrice,
            note: noteText.isEmpty ? null : noteText,
          ),
        );
      }

      if (lineDrafts.isEmpty) {
        SuperportToast.error(context, '최소 1개 이상의 품목을 선택하세요.');
        return;
      }

      final customerDrafts = customerSelection
          .map(
            (draft) => TransactionCustomerDraft(
              id: draft.linkId,
              customerId: draft.option.id,
              note: draft.note,
            ),
          )
          .toList(growable: false);

      if (customerDrafts.isEmpty) {
        SuperportToast.error(context, '최소 1개의 고객사를 선택하세요.');
        return;
      }

      try {
        updateSaving(true);

        if (initialRecord != null) {
          if (transactionId == null) {
            updateSaving(false);
            SuperportToast.error(context, '수정 대상 ID를 확인할 수 없습니다.');
            return;
          }
          final updated = await controller.updateTransaction(
            transactionId,
            StockTransactionUpdateInput(
              transactionStatusId: statusItem.id,
              note: remarkValue,
              expectedReturnDate: returnDue.value,
            ),
            refreshAfter: false,
          );
          final currentLines =
              initialRecord.raw?.lines ?? const <StockTransactionLine>[];
          final currentCustomers =
              initialRecord.raw?.customers ??
              const <StockTransactionCustomer>[];
          final linePlan = _detailSyncService.buildLinePlan(
            drafts: lineDrafts,
            currentLines: currentLines,
          );
          final customerPlan = _detailSyncService.buildCustomerPlan(
            drafts: customerDrafts,
            currentCustomers: currentCustomers,
          );
          if (linePlan.hasChanges) {
            await controller.syncTransactionLines(transactionId, linePlan);
          }
          if (customerPlan.hasChanges) {
            await controller.syncTransactionCustomers(
              transactionId,
              customerPlan,
            );
          }
          await controller.refresh();
          final refreshed = await resolveUpdatedRecord(transactionId);
          result = refreshed ?? updated;
          updateSaving(false);
          if (!mounted) {
            return;
          }
          SuperportToast.success(
            context,
            '대여 정보가 수정되었습니다. (${updated.transactionNumber})',
          );
          navigator.pop();
          return;
        }

        if (createdById == null) {
          updateSaving(false);
          SuperportToast.error(context, '작성자 선택이 필요합니다.');
          return;
        }

        final createLines = lineDrafts
            .map(
              (draft) => TransactionLineCreateInput(
                lineNo: draft.lineNo,
                productId: draft.productId,
                quantity: draft.quantity,
                unitPrice: draft.unitPrice,
                note: draft.note,
              ),
            )
            .toList(growable: false);

        final createCustomers = customerDrafts
            .map(
              (draft) => TransactionCustomerCreateInput(
                customerId: draft.customerId,
                note: draft.note,
              ),
            )
            .toList(growable: false);

        StockTransactionApprovalInput approvalInput;
        try {
          approvalInput = approvalController.buildTransactionApprovalInput(
            approvalStatusId: null,
            note: approvalNoteValue.isEmpty ? null : approvalNoteValue,
          );
        } on StateError catch (error) {
          updateSaving(false);
          SuperportToast.error(context, error.message);
          return;
        } catch (_) {
          updateSaving(false);
          SuperportToast.error(context, '결재 구성을 확인하고 다시 시도하세요.');
          return;
        }
        controller.updateApprovalDraft(approvalInput);

        final transactionTypeId = selectedLookup.id;
        final created = await controller.createTransaction(
          StockTransactionCreateInput(
            transactionTypeId: transactionTypeId,
            transactionStatusId: statusItem.id,
            warehouseId: warehouseId,
            transactionDate: processedAt.value,
            createdById: createdById,
            note: remarkValue,
            expectedReturnDate: returnDue.value,
            lines: createLines,
            customers: createCustomers,
            approval: approvalInput,
          ),
        );
        final refreshed = await resolveUpdatedRecord(created.id);
        result = refreshed ?? created;
        updateSaving(false);
        if (!mounted) {
          return;
        }
        SuperportToast.success(
          context,
          '대여가 등록되었습니다. (${created.transactionNumber})',
        );
        navigator.pop();
      } catch (error) {
        updateSaving(false);
        if (!mounted) {
          return;
        }
        SuperportToast.error(
          context,
          _failureMessage(error, '저장 중 오류가 발생했습니다. 잠시 후 다시 시도하세요.'),
        );
      }
    }

    await showSuperportDialog<void>(
      context: context,
      title: initial == null ? '대여 등록' : '대여 수정',
      description: '대여 기본정보와 반납 예정일, 품목 라인을 입력하세요.',
      constraints: const BoxConstraints(maxWidth: 900, maxHeight: 760),
      onSubmit: handleSubmit,
      actions: [
        StatefulBuilder(
          builder: (context, setState) {
            refreshCancelAction = setState;
            return ShadButton.ghost(
              onPressed: isSaving ? null : () => navigator.pop(),
              child: const Text('취소'),
            );
          },
        ),
        StatefulBuilder(
          builder: (context, setState) {
            refreshSaveAction = setState;
            return ShadButton(
              onPressed: isSaving ? null : () => handleSubmit(),
              child: isSaving
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('저장'),
            );
          },
        ),
      ],
      body: StatefulBuilder(
        builder: (context, setState) {
          refreshForm = setState;
          final theme = ShadTheme.of(context);
          final summary = _RentalDraftSummary.fromDrafts(drafts);
          return SingleChildScrollView(
            padding: const EdgeInsets.only(right: 12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Wrap(
                  spacing: 16,
                  runSpacing: 16,
                  children: [
                    SizedBox(
                      width: 220,
                      child: _FormFieldLabel(
                        label: '처리일자',
                        child: SuperportDatePickerButton(
                          value: processedAt.value,
                          dateFormat: _dateFormatter,
                          firstDate: RentalTableSpec.dateRangeFirstDate,
                          lastDate: RentalTableSpec.dateRangeLastDate,
                          enabled: initial == null,
                          onChanged: (date) {
                            processedAt.value = date;
                            setState(() {});
                          },
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 220,
                      child: _FormFieldLabel(
                        label: '대여 구분',
                        child: ShadSelect<String>(
                          initialValue: rentalTypeValue.value,
                          selectedOptionBuilder: (context, value) =>
                              Text(value),
                          onChanged: (value) {
                            if (value != null) {
                              rentalTypeValue.value = value;
                              transactionTypeController.text =
                                  _transactionTypeForRental(value);
                              setState(() {});
                            }
                          },
                          enabled: initial?.status != '완료',
                          options: RentalTableSpec.rentalTypes
                              .map(
                                (type) =>
                                    ShadOption(value: type, child: Text(type)),
                              )
                              .toList(),
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 220,
                      child: SuperportFormField(
                        label: '창고',
                        required: true,
                        errorText: warehouseError,
                        child: InventoryWarehouseSelectField(
                          initialWarehouseId: initialWarehouseId,
                          enabled: initial == null,
                          onChanged: (option) {
                            warehouseSelection = option;
                            setState(() {
                              if (warehouseError != null) {
                                warehouseError = null;
                              }
                            });
                          },
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 240,
                      child: SuperportFormField(
                        label: '상태',
                        required: true,
                        errorText: statusError,
                        child: ShadSelect<String>(
                          initialValue: statusValue.value,
                          selectedOptionBuilder: (context, value) =>
                              Text(value),
                          onChanged: (value) {
                            if (value != null) {
                              statusValue.value = value;
                              setState(() {
                                if (statusError != null) {
                                  statusError = null;
                                }
                              });
                            }
                          },
                          enabled: initial?.status != '완료',
                          options: _statusOptions
                              .map(
                                (status) => ShadOption(
                                  value: status,
                                  child: Text(status),
                                ),
                              )
                              .toList(),
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 240,
                      child: _FormFieldLabel(
                        label: '트랜잭션 유형',
                        child: ShadInput(
                          controller: transactionTypeController,
                          readOnly: true,
                          enabled: false,
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 240,
                      child: _FormFieldLabel(
                        label: '트랜잭션번호',
                        child: Text(
                          assignedTransactionNo ?? '저장 시 자동 생성',
                          style: assignedTransactionNo == null
                              ? theme.textTheme.muted
                              : theme.textTheme.p,
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 240,
                      child: _FormFieldLabel(
                        label: '결재번호',
                        child: Text(
                          assignedApprovalNo ?? '저장 시 자동 생성',
                          style: assignedApprovalNo == null
                              ? theme.textTheme.muted
                              : theme.textTheme.p,
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 360,
                      child: _FormFieldLabel(
                        label: '대여 고객사',
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            InventoryCustomerMultiSelectField(
                              initialCustomerIds: initialCustomerIds,
                              enabled: initial?.status != '완료',
                              onChanged: (options) {
                                final nextMap =
                                    <int, _RentalSelectedCustomerDraft>{};
                                for (final option in options) {
                                  final existing = customerDraftMap[option.id];
                                  if (existing != null) {
                                    nextMap[option.id] = existing;
                                  } else {
                                    nextMap[option.id] =
                                        _RentalSelectedCustomerDraft(
                                          option: option,
                                        );
                                  }
                                }
                                customerDraftMap
                                  ..clear()
                                  ..addAll(nextMap);
                                customerSelection = customerDraftMap.values
                                    .toList(growable: false);
                                if (customerError != null &&
                                    options.isNotEmpty) {
                                  setState(() {
                                    customerError = null;
                                  });
                                } else {
                                  setState(() {});
                                }
                              },
                            ),
                            if (customerError != null)
                              Padding(
                                padding: const EdgeInsets.only(top: 6),
                                child: Text(
                                  customerError!,
                                  style: theme.textTheme.small.copyWith(
                                    color: theme.colorScheme.destructive,
                                  ),
                                ),
                              ),
                          ],
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 220,
                      child: _FormFieldLabel(
                        label: '반납 예정일',
                        child: SuperportDatePickerButton(
                          value: returnDue.value,
                          dateFormat: _dateFormatter,
                          placeholder: '선택',
                          firstDate: processedAt.value,
                          lastDate: RentalTableSpec.dateRangeLastDate,
                          initialDate: processedAt.value,
                          enabled: initial?.status != '완료',
                          onChanged: (date) {
                            returnDue.value = date;
                            setState(() {});
                          },
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 240,
                      child: _FormFieldLabel(
                        label: '작성자',
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            InventoryEmployeeAutocompleteField(
                              controller: writerController,
                              initialSuggestion: writerSelection,
                              enabled: initial == null,
                              onSuggestionSelected: (suggestion) {
                                writerSelection = suggestion;
                                approvalController.setRequester(
                                  suggestion == null
                                      ? null
                                      : ApprovalRequestParticipant(
                                          id: suggestion.id,
                                          name: suggestion.name,
                                          employeeNo: suggestion.employeeNo,
                                        ),
                                );
                                if (writerError != null) {
                                  setState(() {
                                    writerError = null;
                                  });
                                }
                              },
                              onChanged: () {
                                if (initial == null) {
                                  final currentText = writerController.text
                                      .trim();
                                  final selectedLabel = writerLabel(
                                    writerSelection,
                                  );
                                  if (currentText.isEmpty ||
                                      currentText != selectedLabel) {
                                    writerSelection = null;
                                    approvalController.setRequester(null);
                                  }
                                  if (writerError != null) {
                                    setState(() {
                                      writerError = null;
                                    });
                                  }
                                }
                              },
                            ),
                            if (writerError != null)
                              Padding(
                                padding: const EdgeInsets.only(top: 6),
                                child: Text(
                                  writerError!,
                                  style: theme.textTheme.small.copyWith(
                                    color: theme.colorScheme.destructive,
                                  ),
                                ),
                              ),
                          ],
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 500,
                      child: _FormFieldLabel(
                        label: '결재 메모',
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            ShadInput(
                              controller: approvalNoteController,
                              maxLines: 2,
                              readOnly: initial != null,
                              enabled: initial == null,
                            ),
                            if (initial != null)
                              Padding(
                                padding: const EdgeInsets.only(top: 6),
                                child: Text(
                                  '등록된 결재 메모는 수정할 수 없습니다.',
                                  style: theme.textTheme.small.copyWith(
                                    color: theme.colorScheme.mutedForeground,
                                  ),
                                ),
                              ),
                          ],
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 500,
                      child: _FormFieldLabel(
                        label: '비고',
                        child: ShadInput(
                          controller: remarkController,
                          maxLines: 2,
                        ),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                if (initial == null) ...[
                  ApprovalTemplatePicker(controller: approvalController),
                  const SizedBox(height: 16),
                ],
                ApprovalStepConfigurator(
                  controller: approvalController,
                  readOnly: initial != null,
                ),
                const SizedBox(height: 24),
                Wrap(
                  spacing: 12,
                  runSpacing: 12,
                  children: [
                    _RentalSummaryBadge(
                      icon: lucide.LucideIcons.package,
                      label: '품목 수',
                      value: '${summary.itemCount}건',
                    ),
                    _RentalSummaryBadge(
                      icon: lucide.LucideIcons.trendingUp,
                      label: '총 수량',
                      value: '${summary.totalQuantity} ea',
                    ),
                    _RentalSummaryBadge(
                      icon: lucide.LucideIcons.coins,
                      label: '총 금액',
                      value: _currencyFormatter.format(summary.totalAmount),
                    ),
                    _RentalSummaryBadge(
                      icon: lucide.LucideIcons.users,
                      label: '고객사 수',
                      value: '${customerSelection.length}곳',
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                if (headerNotice != null)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 12),
                    child: Text(
                      headerNotice!,
                      style: theme.textTheme.small.copyWith(
                        color: theme.colorScheme.destructive,
                      ),
                    ),
                  ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('라인 품목', style: theme.textTheme.h4),
                        const SizedBox(height: 4),
                        Text(
                          '제품, 제조사, 단위, 수량, 단가 정보를 입력하세요.',
                          style: theme.textTheme.muted,
                        ),
                      ],
                    ),
                    ShadButton.outline(
                      leading: const Icon(lucide.LucideIcons.plus, size: 16),
                      onPressed: () => setState(() {
                        final draft = _RentalLineItemDraft.empty();
                        drafts.add(draft);
                        lineErrors.putIfAbsent(
                          draft,
                          _RentalLineItemErrors.empty,
                        );
                        headerNotice = null;
                      }),
                      child: const Text('품목 추가'),
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                Column(
                  children: [
                    for (final draft in drafts)
                      Padding(
                        padding: const EdgeInsets.only(bottom: 12),
                        child: _RentalLineItemRow(
                          draft: draft,
                          errors:
                              lineErrors[draft] ??
                              _RentalLineItemErrors.empty(),
                          onFieldChanged: (field) {
                            setState(() {
                              headerNotice = null;
                              final errors = lineErrors[draft];
                              if (errors == null) {
                                lineErrors[draft] =
                                    _RentalLineItemErrors.empty();
                              } else {
                                errors.clearField(field);
                              }
                            });
                          },
                          onRemove: drafts.length == 1
                              ? null
                              : () => setState(() {
                                  draft.dispose();
                                  drafts.remove(draft);
                                  lineErrors.remove(draft);
                                  headerNotice = null;
                                }),
                        ),
                      ),
                  ],
                ),
              ],
            ),
          );
        },
      ),
    );

    for (final draft in drafts) {
      draft.dispose();
    }
    statusValue.dispose();
    rentalTypeValue.dispose();
    writerController.dispose();
    remarkController.dispose();
    transactionTypeController.dispose();
    approvalNoteController.dispose();
    processedAt.dispose();
    returnDue.dispose();
    approvalController.dispose();

    return result;
  }
}

enum _RentalSortField {
  processedAt,
  returnDueDate,
  warehouse,
  status,
  rentalType,
  customerCount,
  itemCount,
  totalQuantity,
}

class _FormFieldLabel extends StatelessWidget {
  const _FormFieldLabel({required this.label, required this.child});

  final String label;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final theme = ShadTheme.of(context);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: theme.textTheme.small),
        const SizedBox(height: 6),
        child,
      ],
    );
  }
}

class _RentalLineItemRow extends StatelessWidget {
  const _RentalLineItemRow({
    required this.draft,
    required this.errors,
    required this.onRemove,
    required this.onFieldChanged,
  });

  final _RentalLineItemDraft draft;
  final _RentalLineItemErrors errors;
  final VoidCallback? onRemove;
  final void Function(_RentalLineField field) onFieldChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: SuperportFormField(
            label: '제품',
            required: true,
            errorText: errors.product,
            child: InventoryProductAutocompleteField(
              productController: draft.product,
              productFocusNode: draft.productFocus,
              manufacturerController: draft.manufacturer,
              unitController: draft.unit,
              initialSuggestion: draft.suggestion,
              onSuggestionSelected: (suggestion) {
                draft.suggestion = suggestion;
                draft.productId = suggestion?.id;
                onFieldChanged(_RentalLineField.product);
              },
              onChanged: () => onFieldChanged(_RentalLineField.product),
            ),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: SuperportFormField(
            label: '제조사',
            caption: '제품 선택 시 자동 입력됩니다.',
            child: ShadInput(
              controller: draft.manufacturer,
              placeholder: const Text('자동 입력'),
              readOnly: true,
              enabled: true,
            ),
          ),
        ),
        const SizedBox(width: 12),
        SizedBox(
          width: 80,
          child: SuperportFormField(
            label: '단위',
            caption: '제품에 연결된 단위입니다.',
            child: ShadInput(
              controller: draft.unit,
              placeholder: const Text('자동'),
              readOnly: true,
              enabled: true,
            ),
          ),
        ),
        const SizedBox(width: 12),
        SizedBox(
          width: 100,
          child: SuperportFormField(
            label: '수량',
            required: true,
            errorText: errors.quantity,
            child: ShadInput(
              controller: draft.quantity,
              placeholder: const Text('수량'),
              keyboardType: TextInputType.number,
              onChanged: (_) => onFieldChanged(_RentalLineField.quantity),
            ),
          ),
        ),
        const SizedBox(width: 12),
        SizedBox(
          width: 120,
          child: SuperportFormField(
            label: '단가',
            required: true,
            errorText: errors.price,
            child: ShadInput(
              controller: draft.price,
              placeholder: const Text('단가'),
              keyboardType: TextInputType.number,
              onChanged: (_) => onFieldChanged(_RentalLineField.price),
            ),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: SuperportFormField(
            label: '비고',
            child: ShadInput(
              controller: draft.remark,
              placeholder: const Text('비고'),
              onChanged: (_) => onFieldChanged(_RentalLineField.remark),
            ),
          ),
        ),
        const SizedBox(width: 12),
        ShadButton.ghost(
          size: ShadButtonSize.sm,
          onPressed: onRemove,
          child: const Icon(lucide.LucideIcons.trash2, size: 16),
        ),
      ],
    );
  }
}

class _RentalLineItemDraft {
  _RentalLineItemDraft._({
    required this.product,
    required this.productFocus,
    required this.manufacturer,
    required this.unit,
    required this.quantity,
    required this.price,
    required this.remark,
    this.lineId,
    this.productId,
  });

  final TextEditingController product;
  final FocusNode productFocus;
  final TextEditingController manufacturer;
  final TextEditingController unit;
  final TextEditingController quantity;
  final TextEditingController price;
  final TextEditingController remark;
  final int? lineId;
  int? productId;
  InventoryProductSuggestion? suggestion;

  factory _RentalLineItemDraft.empty() {
    return _RentalLineItemDraft._(
      product: TextEditingController(),
      productFocus: FocusNode(),
      manufacturer: TextEditingController(),
      unit: TextEditingController(text: 'EA'),
      quantity: TextEditingController(text: '1'),
      price: TextEditingController(text: '0'),
      remark: TextEditingController(),
      lineId: null,
      productId: null,
    );
  }

  factory _RentalLineItemDraft.fromItem(RentalLineItem item) {
    final draft = _RentalLineItemDraft._(
      product: TextEditingController(text: item.product),
      productFocus: FocusNode(),
      manufacturer: TextEditingController(text: item.manufacturer),
      unit: TextEditingController(text: item.unit),
      quantity: TextEditingController(text: '${item.quantity}'),
      price: TextEditingController(text: item.price.toStringAsFixed(0)),
      remark: TextEditingController(text: item.remark),
      lineId: item.id,
      productId: item.productId,
    );
    draft.suggestion = InventoryProductSuggestion(
      id: item.productId,
      code: item.productCode,
      name: item.product,
      vendorName: item.manufacturer,
      unitName: item.unit,
    );
    return draft;
  }

  void dispose() {
    product.dispose();
    productFocus.dispose();
    manufacturer.dispose();
    unit.dispose();
    quantity.dispose();
    price.dispose();
    remark.dispose();
  }
}

enum _RentalLineField { product, quantity, price, remark }

class _RentalLineItemErrors {
  _RentalLineItemErrors();

  String? product;
  String? quantity;
  String? price;

  static _RentalLineItemErrors empty() => _RentalLineItemErrors();

  void clearField(_RentalLineField field) {
    switch (field) {
      case _RentalLineField.product:
        product = null;
        break;
      case _RentalLineField.quantity:
        quantity = null;
        break;
      case _RentalLineField.price:
        price = null;
        break;
      case _RentalLineField.remark:
        break;
    }
  }

  void clearAll() {
    product = null;
    quantity = null;
    price = null;
  }
}

class _RentalSelectedCustomerDraft {
  _RentalSelectedCustomerDraft({required this.option, this.linkId, this.note});

  final InventoryCustomerOption option;
  final int? linkId;
  final String? note;
}

/// 대여 폼의 필수 값 및 품목 조건을 검증한다.
_RentalFormValidation _validateRentalForm({
  required TextEditingController writerController,
  required InventoryEmployeeSuggestion? writerSelection,
  required bool requireWriterSelection,
  required InventoryWarehouseOption? warehouseSelection,
  required String statusValue,
  required List<InventoryCustomerOption> selectedCustomers,
  required List<_RentalLineItemDraft> drafts,
  required Map<_RentalLineItemDraft, _RentalLineItemErrors> lineErrors,
}) {
  var isValid = true;
  String? writerError;
  String? customerError;
  String? warehouseError;
  String? statusError;
  String? headerNotice;

  final writerText = writerController.text.trim();
  if (writerText.isEmpty) {
    writerError = '작성자를 입력하세요.';
    isValid = false;
  }

  if (requireWriterSelection && writerSelection == null) {
    writerError = '작성자는 자동완성에서 선택하세요.';
    isValid = false;
  }

  if (warehouseSelection == null) {
    warehouseError = '창고를 선택하세요.';
    isValid = false;
  }

  if (statusValue.trim().isEmpty) {
    statusError = '상태를 선택하세요.';
    isValid = false;
  }

  if (selectedCustomers.isEmpty) {
    customerError = '최소 1개의 고객사를 선택하세요.';
    isValid = false;
  }

  var hasLineError = false;
  final seenProductKeys = <String>{};
  for (final draft in drafts) {
    final errors = lineErrors.putIfAbsent(draft, _RentalLineItemErrors.empty);
    errors.clearAll();

    final productText = draft.product.text.trim();
    if (productText.isEmpty) {
      errors.product = '제품을 입력하세요.';
      hasLineError = true;
      isValid = false;
    } else if (draft.suggestion == null) {
      errors.product = '제품은 목록에서 선택하세요.';
      hasLineError = true;
      isValid = false;
    }

    final productKey = draft.suggestion?.code ?? productText.toLowerCase();
    if (productKey.isNotEmpty && !seenProductKeys.add(productKey)) {
      errors.product = '동일 제품이 중복되었습니다.';
      hasLineError = true;
      isValid = false;
    }

    final quantity = int.tryParse(
      draft.quantity.text.trim().isEmpty ? '0' : draft.quantity.text.trim(),
    );
    if (quantity == null || quantity < 1) {
      errors.quantity = '수량은 1 이상 정수여야 합니다.';
      hasLineError = true;
      isValid = false;
    }

    final price = _parseCurrency(draft.price.text);
    if (price < 0) {
      errors.price = '단가는 0 이상 숫자여야 합니다.';
      hasLineError = true;
      isValid = false;
    }
  }

  if (hasLineError) {
    headerNotice = '품목 행의 오류를 수정한 후 다시 저장하세요.';
  }

  return _RentalFormValidation(
    isValid: isValid,
    writerError: writerError,
    customerError: customerError,
    warehouseError: warehouseError,
    statusError: statusError,
    headerNotice: headerNotice,
  );
}

class _RentalFormValidation {
  const _RentalFormValidation({
    required this.isValid,
    this.writerError,
    this.customerError,
    this.warehouseError,
    this.statusError,
    this.headerNotice,
  });

  final bool isValid;
  final String? writerError;
  final String? customerError;
  final String? warehouseError;
  final String? statusError;
  final String? headerNotice;
}

double _parseCurrency(String input) {
  final normalized = input.replaceAll(RegExp(r'[^0-9.-]'), '');
  return double.tryParse(normalized.isEmpty ? '0' : normalized) ?? 0;
}

class _RentalDraftSummary {
  const _RentalDraftSummary({
    required this.itemCount,
    required this.totalQuantity,
    required this.totalAmount,
  });

  final int itemCount;
  final int totalQuantity;
  final double totalAmount;

  factory _RentalDraftSummary.fromDrafts(List<_RentalLineItemDraft> drafts) {
    var quantity = 0;
    var amount = 0.0;
    for (final draft in drafts) {
      final qty = int.tryParse(draft.quantity.text.trim()) ?? 0;
      final price = _parseCurrency(draft.price.text);
      quantity += qty;
      amount += price * qty;
    }
    return _RentalDraftSummary(
      itemCount: drafts.length,
      totalQuantity: quantity,
      totalAmount: amount,
    );
  }
}

class _RentalSummaryBadge extends StatelessWidget {
  const _RentalSummaryBadge({
    required this.icon,
    required this.label,
    required this.value,
  });

  final IconData icon;
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    final theme = ShadTheme.of(context);
    return ShadBadge.outline(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon, size: 14, color: theme.colorScheme.mutedForeground),
            const SizedBox(width: 8),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(label, style: theme.textTheme.small),
                const SizedBox(height: 2),
                Text(value, style: theme.textTheme.p),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
