// ignore_for_file: use_build_context_synchronously

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import '../../../core/i18n/app_localizations.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_colors_extension.dart';
import '../../../core/utils/amount_utils.dart';
import '../../../shared/models/bill_file_rights.dart';
import '../expense_api.dart';
import '../../../shared/widgets/attachment_picker.dart';
import '../../../shared/widgets/searchable_picker_sheet.dart';

/// 报销明细输入数据。
class ExpenseDetailInputData {
  final String category;
  final String categoryName;
  final String acctSubjectId;
  final String acctSubjectName;
  final String purpose;
  final double amount;
  final double taxRate;
  final String projectId;
  final String projectName;
  final String costDeptId;
  final String costDeptName;
  final String customerVendorId;
  final String customerVendorName;
  final double approvedAmount;
  final double offsetAmount;
  final String bankName;
  final String bankAccountName;
  final String bankAccount;
  final String remark;
  final List<String> attachmentPaths;
  final String sqMan;
  final String sqManName;
  final String aeNo;
  final String aeDd;

  const ExpenseDetailInputData({
    required this.category,
    required this.categoryName,
    required this.acctSubjectId,
    required this.acctSubjectName,
    required this.purpose,
    required this.amount,
    required this.taxRate,
    this.projectId = '',
    this.projectName = '',
    this.costDeptId = '',
    this.costDeptName = '',
    this.customerVendorId = '',
    this.customerVendorName = '',
    this.approvedAmount = 0.0,
    this.offsetAmount = 0.0,
    this.bankName = '',
    this.bankAccountName = '',
    this.bankAccount = '',
    this.remark = '',
    this.attachmentPaths = const [],
    this.sqMan = '',
    this.sqManName = '',
    this.aeNo = '',
    this.aeDd = '',
  });
}

/// 报销明细编辑弹窗。
///
/// 使用 [TDSlidePopupRoute] 从底部滑出，卡片化展示表单字段。
/// 参照 ExpenseApplyCreatePage 的 ExpenseDetailDialog 样式。
class ExpenseDetailDialog extends StatefulWidget {
  final ExpenseApi api;
  final AppLocalizations l10n;
  final ExpenseDetailInputData? initialData;
  final Future<bool> Function()? checkAttachHealth;
  final bool showAttachments;
  final bool canEditApprovedAmount;
  final BillFileRights? billFileRights;
  final dynamic acctTree;

  const ExpenseDetailDialog({
    super.key,
    required this.api,
    required this.l10n,
    this.initialData,
    this.checkAttachHealth,
    this.showAttachments = true,
    this.canEditApprovedAmount = false,
    this.billFileRights,
    required this.acctTree,
  });

  /// 显示弹窗，返回 [ExpenseDetailInputData] 或 `null`（取消时）。
  static Future<ExpenseDetailInputData?> show(
    BuildContext context, {
    required ExpenseApi api,
    required AppLocalizations l10n,
    ExpenseDetailInputData? initialData,
    Future<bool> Function()? checkAttachHealth,
    bool showAttachments = true,
    bool canEditApprovedAmount = false,
    BillFileRights? billFileRights,
    required dynamic acctTree,
  }) {
    FocusScope.of(context).unfocus();
    return Navigator.push<ExpenseDetailInputData>(
      context,
      TDSlidePopupRoute<ExpenseDetailInputData>(
        slideTransitionFrom: SlideTransitionFrom.bottom,
        isDismissible: true,
        builder: (_) => ExpenseDetailDialog(
          api: api,
          l10n: l10n,
          initialData: initialData,
          checkAttachHealth: checkAttachHealth,
          showAttachments: showAttachments,
          canEditApprovedAmount: canEditApprovedAmount,
          billFileRights: billFileRights,
          acctTree: acctTree,
        ),
      ),
    );
  }

  @override
  State<ExpenseDetailDialog> createState() => _ExpenseDetailDialogState();
}

class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
  late CostProjectItem _selCat;
  late TextEditingController _descCtrl;
  late TextEditingController _amountCtrl;
  CustomerItem? _selCustomer;
  late TextEditingController _approvedAmountCtrl;
  late TextEditingController _remarkCtrl;
  late TextEditingController _bankNameCtrl;
  late TextEditingController _bankAccountNameCtrl;
  late TextEditingController _bankAccountCtrl;
  double _taxRate = 0;
  ProjectCodeItem? _selProject;
  DepartmentItem? _selDept;
  EmployeeItem? _selEmployee;
  late final AttachmentPickerController _attachmentCtrl;
  final ScrollController _scrollCtrl = ScrollController();
  final _remarkFocus = FocusNode();
  final _bankNameFocus = FocusNode();
  final _bankAccountNameFocus = FocusNode();
  final _bankAccountFocus = FocusNode();
  bool _attachAvailable = false;

  void _ensureVisible(FocusNode node) {
    if (!node.hasFocus) return;
    _doEnsureVisible(node, 0, -1);
  }

  void _doEnsureVisible(FocusNode node, int attempt, double lastInsets) {
    if (attempt >= 15) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
      final insets = MediaQuery.of(context).viewInsets.bottom;
      if (insets != lastInsets) {
        _doEnsureVisible(node, attempt + 1, insets);
        return;
      }
      final ctx = node.context;
      if (ctx == null) return;
      Future.delayed(const Duration(milliseconds: 500), () {
        if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
        Scrollable.ensureVisible(
          ctx,
          alignment: 0.5,
          duration: const Duration(milliseconds: 300),
        );
      });
    });
  }

  static const _taxOptions = [0, 6, 9, 13];
  static const _taxLabels = ['0%', '6%', '9%', '13%'];

  AppLocalizations get _l10n => widget.l10n;

  bool get _isEdit => widget.initialData != null;

  @override
  void initState() {
    super.initState();
    final d = widget.initialData;
    _selCat = CostProjectItem(
      idx1: d?.category ?? '',
      name: d?.categoryName ?? '',
      accNo: d?.acctSubjectId ?? '',
      accName: d?.acctSubjectName ?? '',
    );
    _descCtrl = TextEditingController(text: d?.purpose ?? '');
    _amountCtrl = TextEditingController(
      text: d != null && d.amount > 0 ? formatAmountNumeric(d.amount) : '',
    );
    _selCustomer = (d != null && d.customerVendorId.isNotEmpty)
        ? CustomerItem(cusNo: '', name: '')
        : null;
    _approvedAmountCtrl = TextEditingController(
      text: d != null && d.approvedAmount > 0
          ? formatAmountNumeric(d.approvedAmount)
          : '',
    );
    _remarkCtrl = TextEditingController(text: d?.remark ?? '');
    _bankNameCtrl = TextEditingController(text: d?.bankName ?? '');
    _bankAccountNameCtrl = TextEditingController(
      text: d?.bankAccountName ?? '',
    );
    _bankAccountCtrl = TextEditingController(text: d?.bankAccount ?? '');
    _taxRate = d?.taxRate ?? 0;
    _selEmployee = (d != null && d.sqMan.isNotEmpty)
        ? EmployeeItem(salNo: d.sqMan, name: d.sqManName)
        : null;
    _selProject = (d != null && d.projectId.isNotEmpty)
        ? ProjectCodeItem(objNo: d.projectId, name: d.projectName)
        : null;
    _selDept = (d != null && d.costDeptId.isNotEmpty)
        ? DepartmentItem(dep: d.costDeptId, name: d.costDeptName)
        : null;
    if (d != null && d.attachmentPaths.isNotEmpty) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _attachmentCtrl.restoreFromPaths(d.attachmentPaths);
      });
    }
    _attachmentCtrl = AttachmentPickerController(maxCount: 9)
      ..addListener(() => setState(() {}));
    _remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
    _bankNameFocus.addListener(() => _ensureVisible(_bankNameFocus));
    _bankAccountNameFocus.addListener(
      () => _ensureVisible(_bankAccountNameFocus),
    );
    _bankAccountFocus.addListener(() => _ensureVisible(_bankAccountFocus));
    _checkAttachHealth();
  }

  Future<void> _checkAttachHealth() async {
    if (widget.checkAttachHealth == null) return;
    if (mounted) setState(() => _attachAvailable = false);
    try {
      final ok = await widget.checkAttachHealth!();
      if (mounted) setState(() => _attachAvailable = ok);
    } catch (_) {
      if (mounted) setState(() => _attachAvailable = false);
    }
  }

  @override
  void dispose() {
    _descCtrl.dispose();
    _amountCtrl.dispose();
    _approvedAmountCtrl.dispose();
    _remarkCtrl.dispose();
    _bankNameCtrl.dispose();
    _bankAccountNameCtrl.dispose();
    _bankAccountCtrl.dispose();
    _attachmentCtrl.dispose();
    _scrollCtrl.dispose();
    _remarkFocus.dispose();
    _bankNameFocus.dispose();
    _bankAccountNameFocus.dispose();
    _bankAccountFocus.dispose();
    super.dispose();
  }

  void _confirm() {
    final amount = double.tryParse(_amountCtrl.text) ?? 0;
    if (amount <= 0) {
      TDToast.showText(_l10n.get('amountPositive'), context: context);
      return;
    }
    Navigator.pop(
      context,
      ExpenseDetailInputData(
        category: _selCat.idx1,
        categoryName: _selCat.name,
        acctSubjectId: _selCat.accNo,
        acctSubjectName: _selCat.accName,
        purpose: '',
        amount: amount,
        taxRate: _taxRate,
        projectId: _selProject?.objNo ?? '',
        projectName: _selProject?.name ?? '',
        costDeptId: _selDept?.dep ?? '',
        costDeptName: _selDept?.name ?? '',
        customerVendorId: _selCustomer?.cusNo ?? '',
        customerVendorName: _selCustomer?.name ?? '',
        approvedAmount: double.tryParse(_approvedAmountCtrl.text) ?? 0,
        bankName: _bankNameCtrl.text.trim(),
        bankAccountName: _bankAccountNameCtrl.text.trim(),
        bankAccount: _bankAccountCtrl.text.trim(),
        remark: _remarkCtrl.text.trim(),
        attachmentPaths: _attachmentCtrl.toPathList(),
        sqMan: _selEmployee?.salNo ?? '',
        sqManName: _selEmployee?.name ?? '',
        aeNo: widget.initialData?.aeNo ?? '',
        aeDd: widget.initialData?.aeDd ?? '',
      ),
    );
  }

  double get _amountExclTax => _taxRate > 0
      ? (double.tryParse(_amountCtrl.text) ?? 0) / (1 + _taxRate / 100)
      : (double.tryParse(_amountCtrl.text) ?? 0);
  double get _taxAmount =>
      (double.tryParse(_amountCtrl.text) ?? 0) - _amountExclTax;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).extension<AppColorsExtension>()!;

    return AnimatedPadding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
      ),
      duration: const Duration(milliseconds: 200),
      child: SafeArea(
        child: ConstrainedBox(
          constraints: BoxConstraints(
            maxHeight: MediaQuery.of(context).size.height * 0.8,
          ),
          child: Container(
            decoration: BoxDecoration(
              color: colors.bgPage,
              borderRadius: const BorderRadius.vertical(
                top: Radius.circular(16),
              ),
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                _buildHeader(colors),
                Flexible(
                  child: GestureDetector(
                    onTap: () => FocusScope.of(context).unfocus(),
                    behavior: HitTestBehavior.translucent,
                    child: SingleChildScrollView(
                      controller: _scrollCtrl,
                      keyboardDismissBehavior:
                          ScrollViewKeyboardDismissBehavior.manual,
                      padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          if (_isEdit &&
                              widget.initialData!.aeNo.isNotEmpty) ...[
                            _buildAeInfoCard(colors),
                            const SizedBox(height: 12),
                          ],
                          _buildCategoryCard(colors),
                          const SizedBox(height: 12),
                          _buildAcctSubjectCard(colors),
                          const SizedBox(height: 12),
                          _buildAmountCard(),
                          const SizedBox(height: 12),
                          _buildTaxRateCard(colors),
                          if ((double.tryParse(_amountCtrl.text) ?? 0) > 0) ...[
                            const SizedBox(height: 12),
                            _buildCalcInfo(colors),
                          ],
                          const SizedBox(height: 12),
                          _buildApprovedAmountCard(),
                          const SizedBox(height: 12),
                          _buildProjectCard(colors),
                          const SizedBox(height: 12),
                          _buildCostDeptCard(colors),
                          const SizedBox(height: 12),
                          _buildCustomerCard(colors),
                          const SizedBox(height: 12),
                          _buildEmployeeCard(colors),
                          const SizedBox(height: 12),
                          _buildBankInfoCard(colors),
                          const SizedBox(height: 12),
                          _buildRemarkInput(colors),
                          if (widget.showAttachments) ...[
                            const SizedBox(height: 12),
                            _buildAttachmentCard(colors),
                          ],
                        ],
                      ),
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
                  decoration: BoxDecoration(
                    color: colors.bgCard,
                    border: Border(
                      top: BorderSide(color: colors.border, width: 0.5),
                    ),
                  ),
                  child: _buildActions(),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  // ── 标题栏 ──
  Widget _buildHeader(AppColorsExtension colors) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Center(
          child: Container(
            margin: const EdgeInsets.only(top: 8, bottom: 4),
            width: 36,
            height: 4,
            decoration: BoxDecoration(
              color: colors.border,
              borderRadius: BorderRadius.circular(2),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(20, 8, 12, 16),
          child: Row(
            children: [
              const SizedBox(width: 24),
              Expanded(
                child: Center(
                  child: Text(
                    _l10n.get('addExpenseDetail'),
                    style: TextStyle(
                      fontSize: AppFontSizes.title,
                      fontWeight: FontWeight.w600,
                      color: colors.textPrimary,
                    ),
                  ),
                ),
              ),
              GestureDetector(
                onTap: () => Navigator.pop(context),
                child: Padding(
                  padding: const EdgeInsets.all(4),
                  child: Icon(
                    Icons.close,
                    size: 20,
                    color: colors.textSecondary,
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  // ── picker 卡片 ──
  Widget _pickerCard({
    required String label,
    required bool required,
    required String currentLabel,
    required bool hasValue,
    required VoidCallback onTap,
    VoidCallback? onClear,
  }) {
    final tdTheme = TDTheme.of(context);
    return GestureDetector(
      onTap: () {
        FocusManager.instance.primaryFocus?.unfocus();
        onTap();
      },
      child: Container(
        padding: const EdgeInsets.only(
          left: 16,
          right: 10,
          top: 12,
          bottom: 12,
        ),
        decoration: BoxDecoration(
          color: tdTheme.bgColorContainer,
          borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
          border: Border.all(color: tdTheme.componentStrokeColor),
        ),
        child: Row(
          children: [
            TDText(
              label,
              maxLines: 1,
              overflow: TextOverflow.visible,
              font: tdTheme.fontBodyLarge,
              fontWeight: FontWeight.w400,
              style: const TextStyle(letterSpacing: 0),
            ),
            if (required)
              Padding(
                padding: const EdgeInsets.only(left: 4),
                child: TDText(
                  '*',
                  font: tdTheme.fontBodyLarge,
                  fontWeight: FontWeight.w400,
                  style: TextStyle(color: tdTheme.errorColor6),
                ),
              ),
            const SizedBox(width: 12),
            Expanded(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.end,
                mainAxisSize: MainAxisSize.max,
                children: [
                  Flexible(
                    child: TDText(
                      currentLabel,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      font: tdTheme.fontBodyLarge,
                      fontWeight: FontWeight.w400,
                      textColor: hasValue ? tdTheme.textColorPrimary : tdTheme.textColorPlaceholder,
                      textAlign: TextAlign.end,
                    ),
                  ),
                  const SizedBox(width: 4),
                  SizedBox(
                    width: 18,
                    height: 18,
                    child: onClear != null
                        ? GestureDetector(
                            onTap: onClear,
                            child: Icon(
                              Icons.close,
                              size: 18,
                              color: tdTheme.textColorPlaceholder,
                            ),
                          )
                        : Icon(
                            Icons.chevron_right,
                            size: 18,
                            color: tdTheme.textColorPlaceholder,
                          ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  // ── 费用项目 ──
  Widget _buildCategoryCard(AppColorsExtension colors) {
    return _pickerCard(
      label: _l10n.get('expenseCategory'),
      required: true,
      hasValue: _selCat.idx1.isNotEmpty,
      currentLabel: _selCat.idx1.isNotEmpty
          ? '${_selCat.idx1}/${_selCat.name}'
          : _l10n.get('pleaseSelect'),
      onTap: () async {
        final result = await showSearchablePicker<CostProjectItem>(
          context,
          title: '${_l10n.get('select')}${_l10n.get('expenseCategory')}',
          searchHint: _l10n.get('search'),
          loader: (keyword, page) => widget.api
              .getCostProjects(keyword: keyword, page: page, size: 20)
              .then((list) => list.cast<CostProjectItem>()),
          labelBuilder: (c) => '${c.idx1}/${c.name}',
          onRefresh: () => widget.api.clearRefCache(),
        );
        if (result != null && mounted) {
          setState(() => _selCat = result);
        }
      },
    );
  }

  // ── 输入卡片（对齐 pickerCard 样式） ──
  Widget _inputCard({
    required String label,
    required bool required,
    required TextEditingController controller,
    required String hintText,
    required AppColorsExtension colors,
    TextInputType? keyboardType,
    List<TextInputFormatter>? inputFormatters,
    FocusNode? focusNode,
  }) {
    final tdTheme = TDTheme.of(context);
    final hasValue = controller.text.isNotEmpty;
    return Container(
      padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
      decoration: BoxDecoration(
        color: tdTheme.bgColorContainer,
        borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
        border: Border.all(color: tdTheme.componentStrokeColor),
      ),
      child: Row(
        children: [
          TDText(
            label,
            maxLines: 1,
            overflow: TextOverflow.visible,
            font: tdTheme.fontBodyLarge,
            fontWeight: FontWeight.w400,
            style: const TextStyle(letterSpacing: 0),
          ),
          if (required)
            Padding(
              padding: const EdgeInsets.only(left: 4),
              child: TDText(
                '*',
                font: tdTheme.fontBodyLarge,
                fontWeight: FontWeight.w400,
                style: TextStyle(color: tdTheme.errorColor6),
              ),
            ),
          const SizedBox(width: 12),
          Expanded(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              mainAxisSize: MainAxisSize.max,
              children: [
                Flexible(
                  child: TextField(
                    controller: controller,
                    focusNode: focusNode,
                    textAlign: TextAlign.end,
                    keyboardType: keyboardType,
                    inputFormatters: inputFormatters,
                    style: TextStyle(fontSize: 16, color: colors.textPrimary),
                    decoration: InputDecoration(
                      hintText: hintText,
                      hintStyle: TextStyle(
                        fontSize: 16,
                        color: colors.textPlaceholder,
                      ),
                      border: InputBorder.none,
                      isDense: true,
                      contentPadding: EdgeInsets.zero,
                    ),
                    onChanged: (_) => setState(() {}),
                  ),
                ),
                const SizedBox(width: 4),
                SizedBox(
                  width: 18,
                  height: 18,
                  child: hasValue
                      ? GestureDetector(
                          onTap: () {
                            controller.clear();
                            setState(() {});
                          },
                          child: Icon(
                            Icons.close,
                            size: 18,
                            color: tdTheme.textColorPlaceholder,
                          ),
                        )
                      : null,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  // ── 含税金额 ──
  Widget _buildAmountCard() {
    return _inputCard(
      label: _l10n.get('amountInclTax'),
      required: true,
      controller: _amountCtrl,
      hintText: '>0',
      colors: Theme.of(context).extension<AppColorsExtension>()!,
      keyboardType: const TextInputType.numberWithOptions(decimal: true),
      inputFormatters: [
        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
      ],
    );
  }

  // ── 税率 ──
  Widget _buildTaxRateCard(AppColorsExtension colors) {
    final currentLabel = '${_taxRate.toStringAsFixed(0)}%';
    final labels = _taxLabels.toList();
    return _pickerCard(
      label: _l10n.get('taxRate'),
      required: false,
      hasValue: true,
      currentLabel: currentLabel,
      onTap: () {
        FocusManager.instance.primaryFocus?.unfocus();
        TDPicker.showMultiPicker(
          context,
          title: _l10n.get('taxRate'),
          backgroundColor: colors.bgCard,
          data: [labels],
          onConfirm: (selected) {
            if (selected.isNotEmpty && selected[0] is int) {
              final idx = selected[0] as int;
              if (idx >= 0 && idx < labels.length) {
                Navigator.of(context).pop();
                setState(() => _taxRate = _taxOptions[idx].toDouble());
              }
            }
          },
        );
      },
    );
  }

  // ── 计算信息 ──
  Widget _buildCalcInfo(AppColorsExtension colors) {
    final amount = double.tryParse(_amountCtrl.text) ?? 0;
    if (amount <= 0) return const SizedBox.shrink();
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: colors.primaryLight,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          Expanded(
            child: Text(
              '${_l10n.get('amountExcludingTax')}: ${formatAmount(_amountExclTax)}',
              style: TextStyle(
                fontSize: AppFontSizes.body,
                color: colors.textSecondary,
              ),
            ),
          ),
          Text(
            '${_l10n.get('taxAmount')}: ${formatAmount(_taxAmount)}',
            style: TextStyle(
              fontSize: AppFontSizes.body,
              color: colors.textSecondary,
            ),
          ),
        ],
      ),
    );
  }

  // ── 导入单据信息 ──
  Widget _buildAeInfoCard(AppColorsExtension colors) {
    final d = widget.initialData!;
    final tdTheme = TDTheme.of(context);
    final dateStr = d.aeDd.isNotEmpty ? _formatDate(d.aeDd) : d.aeDd;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: _cardDecoration(tdTheme),
      child: Column(
        children: [
          _readOnlyRow(tdTheme, _l10n.get('expenseApplyNo'), d.aeNo),
          const SizedBox(height: 8),
          _readOnlyRow(tdTheme, _l10n.get('applyDate'), dateStr),
        ],
      ),
    );
  }

  String _formatDate(String raw) {
    final dt = DateTime.tryParse(raw);
    if (dt == null) return raw;
    return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
  }

  Widget _readOnlyRow(TDThemeData tdTheme, String label, String value) {
    return Row(
      children: [
        TDText(
          label,
          font: tdTheme.fontBodyLarge,
          fontWeight: FontWeight.w400,
          style: const TextStyle(letterSpacing: 0),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: TDText(
            value,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            font: tdTheme.fontBodyLarge,
            fontWeight: FontWeight.w400,
            textColor: tdTheme.textColorPrimary,
            textAlign: TextAlign.end,
          ),
        ),
      ],
    );
  }

  BoxDecoration _cardDecoration(TDThemeData tdTheme) => BoxDecoration(
    color: tdTheme.bgColorContainer,
    borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
    border: Border.all(color: tdTheme.componentStrokeColor),
  );

  // ── 申请人 ──
  Widget _buildEmployeeCard(AppColorsExtension colors) {
    return _pickerCard(
      label: _l10n.get('applicant'),
      required: false,
      hasValue: _selEmployee != null,
      currentLabel: _selEmployee != null
          ? '${_selEmployee!.salNo}/${_selEmployee!.name}'
          : _l10n.get('pleaseSelect'),
      onTap: () async {
        final result = await showSearchablePicker<EmployeeItem>(
          context,
          title: '${_l10n.get('select')}${_l10n.get('applicant')}',
          searchHint: _l10n.get('search'),
          loader: (keyword, page) =>
              widget.api.getEmployees(keyword: keyword, page: page, size: 20),
          labelBuilder: (e) =>
              e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}',
          onRefresh: () => widget.api.clearRefCache(),
        );
        if (result != null && mounted) {
          setState(() {
            _selEmployee = result;
            _bankNameCtrl.text = result.bnkNo;
            _bankAccountNameCtrl.text = result.accName;
            _bankAccountCtrl.text = result.bnkId;
          });
        }
      },
      onClear: _selEmployee != null
          ? () => setState(() {
              _selEmployee = null;
              _bankNameCtrl.clear();
              _bankAccountNameCtrl.clear();
              _bankAccountCtrl.clear();
            })
          : null,
    );
  }

  // ── 客户/厂商 ──
  Widget _buildCustomerCard(AppColorsExtension colors) {
    return _pickerCard(
      label: _l10n.get('customerVendor'),
      required: false,
      hasValue: _selCustomer != null,
      currentLabel: _selCustomer != null
          ? '${_selCustomer!.cusNo}/${_selCustomer!.name}'
          : _l10n.get('pleaseSelect'),
      onTap: () async {
        final result = await showSearchablePicker<CustomerItem>(
          context,
          title: '${_l10n.get('select')}${_l10n.get('customerVendor')}',
          searchHint: _l10n.get('search'),
          loader: (keyword, page) =>
              widget.api.getCustomers(keyword: keyword, page: page, size: 20),
          labelBuilder: (v) =>
              v.name.isEmpty ? v.cusNo : '${v.cusNo} ${v.name}',
          onRefresh: () => widget.api.clearRefCache(),
        );
        if (result != null && mounted) {
          setState(() => _selCustomer = result);
        }
      },
      onClear: _selCustomer != null
          ? () => setState(() => _selCustomer = null)
          : null,
    );
  }

  // ── 核准金额 ──
  Widget _buildApprovedAmountCard() {
    if (!widget.canEditApprovedAmount) {
      final value = _approvedAmountCtrl.text.isNotEmpty
          ? formatAmount(double.tryParse(_approvedAmountCtrl.text) ?? 0)
          : '-';
      return _readOnlyCard(
        label: _l10n.get('approvedAmount'),
        value: value,
        colors: Theme.of(context).extension<AppColorsExtension>()!,
      );
    }
    return _inputCard(
      label: _l10n.get('approvedAmount'),
      required: false,
      controller: _approvedAmountCtrl,
      hintText: '0',
      colors: Theme.of(context).extension<AppColorsExtension>()!,
      keyboardType: const TextInputType.numberWithOptions(decimal: true),
      inputFormatters: [
        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
      ],
    );
  }

  // ── 备注 ──
  Widget _buildRemarkInput(AppColorsExtension colors) {
    final tdTheme = TDTheme.of(context);
    return TDTextarea(
      controller: _remarkCtrl,
      focusNode: _remarkFocus,
      label: _l10n.get('remark'),
      hintText: _l10n.get('enterRemark'),
      maxLines: 3,
      minLines: 1,
      maxLength: 500,
      indicator: true,
      decoration: BoxDecoration(
        color: tdTheme.bgColorContainer,
        borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
        border: Border.all(color: tdTheme.componentStrokeColor),
      ),
      onChanged: (_) => setState(() {}),
    );
  }

  // ── 操作按钮 ──
  Widget _buildAttachmentCard(AppColorsExtension colors) {
    final tdTheme = TDTheme.of(context);
    Widget? hint;
    if (!_attachAvailable) {
      hint = TDText(
        _l10n.get('attachServiceUnavailable'),
        font: tdTheme.fontBodySmall,
        fontWeight: FontWeight.w400,
        textColor: tdTheme.textColorPlaceholder,
      );
    } else if (widget.billFileRights != null &&
        !widget.billFileRights!.canManageAttachments) {
      hint = TDText(
        _l10n.get('noAttachmentPermission'),
        font: tdTheme.fontBodySmall,
        fontWeight: FontWeight.w400,
        textColor: tdTheme.textColorPlaceholder,
      );
    }

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.only(left: 4),
          child: TDText(
            _l10n.get('attachmentUpload'),
            font: tdTheme.fontBodyLarge,
            fontWeight: FontWeight.w400,
            style: const TextStyle(letterSpacing: 0),
          ),
        ),
        const SizedBox(height: 4),
        if (hint != null) ...[
          Padding(padding: const EdgeInsets.only(left: 4), child: hint),
        ] else ...[
          Padding(
            padding: const EdgeInsets.only(left: 4, top: 4),
            child: TDText(
              _l10n.get('maxAttachment'),
              font: tdTheme.fontBodySmall,
              fontWeight: FontWeight.w400,
              textColor: tdTheme.textColorPlaceholder,
            ),
          ),
          const SizedBox(height: 4),
          AttachmentPicker(
            controller: _attachmentCtrl,
            maxImageSizeMB: 10,
            maxFileSizeMB: 20,
            allowedExtensions: const [
              'pdf',
              'doc',
              'docx',
              'xls',
              'xlsx',
              'ppt',
              'pptx',
              'txt',
            ],
            onFileRejected: (file, reason) {
              if (context.mounted) TDToast.showText(reason, context: context);
            },
          ),
        ],
      ],
    );
  }

  // ── 会计科目（级联选择器，支持手动修改） ──
  Widget _buildAcctSubjectCard(AppColorsExtension colors) {
    final tdTheme = TDTheme.of(context);
    final hasValue = _selCat.accNo.isNotEmpty;
    final displayValue = hasValue
        ? (_selCat.accName.startsWith(_selCat.accNo)
              ? _selCat.accName
              : (_selCat.accName.toString().isNotEmpty
                    ? '${_selCat.accNo}/${_selCat.accName}'
                    : _selCat.accNo))
        : _l10n.get('pleaseSelect');
    return GestureDetector(
      onTap: () {
        if (widget.acctTree == null) {
          TDToast.showText(_l10n.get('loading'), context: context);
          return;
        }
        FocusManager.instance.primaryFocus?.unfocus();
        TDCascader.showMultiCascader(
          context,
          title: _l10n.get('acctSubject'),
          data: widget.acctTree as List<Map>,
          theme: 'step',
          onChange: (List<MultiCascaderListModel> selected) {
            setState(() {
              _selCat = CostProjectItem(
                idx1: _selCat.idx1,
                name: _selCat.name,
                accNo: (selected.last.value ?? '').replaceFirst('self:', ''),
                accName: (selected.last.label ?? '')
                    .replaceFirst(RegExp(r'^> '), '')
                    .replaceFirst('${selected.last.value ?? ''}/', ''),
              );
            });
          },
        );
      },
      child: Container(
        padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
        decoration: BoxDecoration(
          color: tdTheme.bgColorContainer,
          borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
          border: Border.all(color: tdTheme.componentStrokeColor),
        ),
        child: Row(
          children: [
            TDText(
              _l10n.get('acctSubject'),
              maxLines: 1,
              overflow: TextOverflow.visible,
              font: tdTheme.fontBodyLarge,
              fontWeight: FontWeight.w400,
              style: const TextStyle(letterSpacing: 0),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.end,
                mainAxisSize: MainAxisSize.max,
                children: [
                  Flexible(
                    child: TDText(
                      displayValue,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      font: tdTheme.fontBodyLarge,
                      fontWeight: FontWeight.w400,
                      textColor: hasValue ? tdTheme.textColorPrimary : tdTheme.textColorPlaceholder,
                      textAlign: TextAlign.end,
                    ),
                  ),
                  const SizedBox(width: 4),
                  SizedBox(
                    width: 18,
                    height: 18,
                    child: hasValue
                        ? GestureDetector(
                            onTap: () {
                              setState(() {
                                _selCat = CostProjectItem(
                                  idx1: _selCat.idx1,
                                  name: _selCat.name,
                                  accNo: '',
                                  accName: '',
                                );
                              });
                            },
                            child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
                          )
                        : Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _readOnlyCard({
    required String label,
    required String value,
    required AppColorsExtension colors,
  }) {
    final tdTheme = TDTheme.of(context);
    return Container(
      padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 12),
      decoration: BoxDecoration(
        color: tdTheme.bgColorContainer,
        borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
        border: Border.all(color: tdTheme.componentStrokeColor),
      ),
      child: Row(
        children: [
          TDText(
            label,
            maxLines: 1,
            overflow: TextOverflow.visible,
            font: tdTheme.fontBodyLarge,
            fontWeight: FontWeight.w400,
            style: const TextStyle(letterSpacing: 0),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: TDText(
              value,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              font: tdTheme.fontBodyLarge,
              fontWeight: FontWeight.w400,
              textColor: tdTheme.textColorPrimary,
              textAlign: TextAlign.end,
            ),
          ),
        ],
      ),
    );
  }

  // ── 关联项目 ──
  Widget _buildProjectCard(AppColorsExtension colors) {
    return _pickerCard(
      label: _l10n.get('relatedProject'),
      required: false,
      hasValue: _selProject != null,
      currentLabel: _selProject != null
          ? '${_selProject!.objNo}/${_selProject!.name}'
          : _l10n.get('pleaseSelect'),
      onTap: () async {
        final result = await showSearchablePicker<ProjectCodeItem>(
          context,
          title: '${_l10n.get('select')}${_l10n.get('relatedProject')}',
          searchHint: _l10n.get('search'),
          loader: (keyword, page) => widget.api
              .getProjectCodes(keyword: keyword, page: page, size: 20)
              .then((list) => list.cast<ProjectCodeItem>()),
          labelBuilder: (p) => '${p.objNo}/${p.name}',
          onRefresh: () => widget.api.clearRefCache(),
        );
        if (result != null && mounted) {
          setState(() => _selProject = result);
        }
      },
      onClear: _selProject != null
          ? () => setState(() => _selProject = null)
          : null,
    );
  }

  // ── 费用承担部门 ──
  Widget _buildCostDeptCard(AppColorsExtension colors) {
    return _pickerCard(
      label: _l10n.get('costDept'),
      required: false,
      hasValue: _selDept != null,
      currentLabel: _selDept != null
          ? '${_selDept!.dep}/${_selDept!.name}'
          : _l10n.get('pleaseSelect'),
      onTap: () async {
        final result = await showSearchablePicker<DepartmentItem>(
          context,
          title: '${_l10n.get('select')}${_l10n.get('costDept')}',
          searchHint: _l10n.get('search'),
          loader: (keyword, page) =>
              widget.api.getDepartments(keyword: keyword, page: page, size: 20),
          labelBuilder: (d) => d.name.isEmpty ? d.dep : '${d.dep} ${d.name}',
          onRefresh: () => widget.api.clearRefCache(),
        );
        if (result != null && mounted) {
          setState(() => _selDept = result);
        }
      },
      onClear: _selDept != null ? () => setState(() => _selDept = null) : null,
    );
  }

  // ── 收款银行信息 ──
  Widget _buildBankInfoCard(AppColorsExtension colors) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _inputCard(
          label: _l10n.get('bankName'),
          required: false,
          controller: _bankNameCtrl,
          hintText: _l10n.get('pleaseEnter'),
          colors: colors,
          focusNode: _bankNameFocus,
        ),
        const SizedBox(height: 12),
        _inputCard(
          label: _l10n.get('bankAccountName'),
          required: false,
          controller: _bankAccountNameCtrl,
          hintText: _l10n.get('pleaseEnter'),
          colors: colors,
          focusNode: _bankAccountNameFocus,
        ),
        const SizedBox(height: 12),
        _inputCard(
          label: _l10n.get('bankAccount'),
          required: false,
          controller: _bankAccountCtrl,
          hintText: _l10n.get('pleaseEnter'),
          colors: colors,
          focusNode: _bankAccountFocus,
        ),
      ],
    );
  }

  Widget _buildActions() {
    return Row(
      children: [
        Expanded(
          child: TDButton(
            text: _l10n.get('cancel'),
            size: TDButtonSize.large,
            type: TDButtonType.outline,
            shape: TDButtonShape.rectangle,
            theme: TDButtonTheme.defaultTheme,
            onTap: () => Navigator.pop(context),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: TDButton(
            text: _isEdit ? _l10n.get('confirmEdit') : _l10n.get('add'),
            size: TDButtonSize.large,
            type: TDButtonType.fill,
            shape: TDButtonShape.rectangle,
            theme: TDButtonTheme.primary,
            onTap: _confirm,
          ),
        ),
      ],
    );
  }
}
