import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:dio/dio.dart';
import 'package:go_router/go_router.dart';
import '../../core/network/api_exception.dart';
import '../../core/utils/responsive.dart';
import '../../shared/widgets/form_section.dart';
import '../../shared/widgets/form_field_row.dart';
import '../../shared/widgets/app_skeletons.dart';
import '../../shared/widgets/nav_bar_config.dart';
import '../../shared/widgets/searchable_picker_sheet.dart';
import 'expense_api.dart';
import 'expense_create_controller.dart';
import '../../core/i18n/app_localizations.dart';
import 'expense_model.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_colors_extension.dart';
import '../../core/navigation/host_app_channel.dart';
import '../../core/utils/amount_utils.dart';
import 'widgets/expense_detail_dialog.dart';
import '../../shared/models/bill_file_rights.dart';
import '../../shared/widgets/action_bar.dart';
import '../../shared/widgets/loading_dialog.dart';
import '../../shared/widgets/attachment_picker.dart';
import 'expense_apply_import_page.dart';

class ExpenseCreatePage extends ConsumerStatefulWidget {
  final String? editId;
  const ExpenseCreatePage({super.key, this.editId});

  @override
  ConsumerState<ExpenseCreatePage> createState() => _ExpenseCreatePageState();
}

class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
  final _purposeController = TextEditingController();
  final _purposeFocus = FocusNode();
  final _remarkController = TextEditingController();
  final _remarkFocus = FocusNode();
  final _scrollCtrl = ScrollController();
  final _detailsSectionKey = GlobalKey();
  late final AttachmentPickerController _attachmentController;
  bool _attachAvailable = false;
  BillFileRights _billFileRights = BillFileRights.none;
  late Future<bool> _draftFuture;
  bool _draftHandled = false;
  // ── 参考数据（从 API 加载） ──
  List<DepartmentItem> _departments = [];
  List<CurrencyItem> _currencies = [];
  List<EmployeeItem> _employees = [];
  EmployeeItem? _selEmployee;
  dynamic _acctTree;
  bool _firstBuild = true;
  bool _refDataLoading = true;
  bool _addingDetail = false;
  bool _canEditApprovedAmount = false;

  // ── 已导入申请单号集合，用于申请事由去重拼接 ──
  final _importedAeNos = <String>{};

  // ── 报销部门 ──
  String _selectedDeptId = '';
  String _selectedDeptName = '';

  @override
  void initState() {
    super.initState();
    SystemChrome.setSystemUIOverlayStyle(
      const SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        statusBarIconBrightness: Brightness.dark,
      ),
    );
    _attachmentController = AttachmentPickerController(maxCount: 9)
      ..addListener(() => setState(() {}));
    _checkAttachHealth();
    _checkApprovedAmountRight();
    _checkBillFileRights();
    _purposeFocus.addListener(() => _ensureVisible(_purposeFocus));
    _remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
    _departments = [];
    _employees = [];
    _refDataLoading = true;
    _refDataFuture = null;
    _draftFuture = widget.editId == null
        ? ExpenseCreateController.hasDraft()
        : Future.value(false);
    _loadRefData();
    WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
  }

  void _checkDataReady() {
    if (!_refDataLoading && mounted) {
      setState(() => _firstBuild = false);
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted) setState(() {});
      });
    } else if (mounted) {
      WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
    }
  }

  Future<void>? _refDataFuture;

  Future<void> _loadRefData() async {
    if (_refDataFuture != null) return _refDataFuture!;
    final completer = Completer<void>();
    _refDataFuture = completer.future;
    try {
      final api = ref.read(expenseApiProvider);
      final results = await Future.wait([
        api.getDepartments(),
        api.getCurrencies(),
        api.getEmployees(),
        api.getAcctSubjects(),
      ]);
      if (!mounted) {
        completer.complete();
        return;
      }
      setState(() {
        _departments = results[0] as List<DepartmentItem>;
        _currencies = results[1] as List<CurrencyItem>;
        _employees = results[2] as List<EmployeeItem>;
        _acctTree = _convertAcctTree(results[3]);
        _refDataLoading = false;
        _autoSelectDept();
        _autoSelectEmployee();
      });
      completer.complete();
    } catch (_) {
      if (!mounted) {
        completer.complete();
        return;
      }
      setState(() => _refDataLoading = false);
      completer.complete();
    } finally {
      _refDataFuture = null;
    }
  }

  void _scrollToDetailSection() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollCtrl.hasClients && _detailsSectionKey.currentContext != null) {
        Scrollable.ensureVisible(
          _detailsSectionKey.currentContext!,
          alignment: 0.0,
          duration: const Duration(milliseconds: 300),
        );
      }
    });
  }

  void _ensureVisible(FocusNode node) {
    if (!node.hasFocus) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (node.hasFocus && _scrollCtrl.hasClients) {
        final ctx = node.context;
        if (ctx != null) {
          Scrollable.ensureVisible(
            ctx,
            alignment: 0.3,
            duration: const Duration(milliseconds: 300),
          );
        }
      }
    });
  }

  @override
  void dispose() {
    _purposeController.dispose();
    _purposeFocus.dispose();
    _remarkController.dispose();
    _remarkFocus.dispose();
    _scrollCtrl.dispose();
    _attachmentController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final controller = ref.watch(expenseCreateProvider(widget.editId).notifier);
    final state = ref.watch(expenseCreateProvider(widget.editId));
    final r = ResponsiveHelper.of(context);
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final bottomInset = MediaQuery.of(context).padding.bottom;

    if (_firstBuild) return const SkeletonFormPage(showImportLink: true);

    Future.microtask(
      () => ref.read(pageBackProvider.notifier).state = () => _doPop(),
    );
    Widget pageContent = PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, _) {
        if (didPop) return;
        _doPop();
      },
      child: Column(
        children: [
          Expanded(
            child: Align(
              alignment: Alignment.topCenter,
              child: ConstrainedBox(
                constraints: BoxConstraints(maxWidth: r.formMaxWidth),
                child: SingleChildScrollView(
                  controller: _scrollCtrl,
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildImportLink(),
                      const SizedBox(height: 16),
                      _buildBasicInfoSection(controller, state),
                      const SizedBox(height: 16),
                      Container(
                        key: _detailsSectionKey,
                        child: _buildDetailSection(controller, state),
                      ),
                      const SizedBox(height: 16),
                      _buildAttachmentSection(controller, state),
                      const SizedBox(height: 24),
                      _buildPageFooter(),
                    ],
                  ),
                ),
              ),
            ),
          ),
          ColoredBox(
            color: colors.bgCard,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                _buildBottomButtons(controller, state),
                if (bottomInset > 0) SizedBox(height: bottomInset),
              ],
            ),
          ),
        ],
      ),
    );

    return FutureBuilder<bool>(
      future: _draftFuture,
      builder: (ctx, snapshot) {
        final hasDraft = snapshot.hasData && snapshot.data == true;

        if (hasDraft && !_draftHandled) {
          _draftHandled = true;
          WidgetsBinding.instance.addPostFrameCallback((_) {
            if (mounted) _showDraftDialog();
          });
        }

        return pageContent;
      },
    );
  }

  void _showDraftDialog() {
    final l10n = AppLocalizations.of(context);
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    FocusManager.instance.primaryFocus?.unfocus();
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (ctx) => TDAlertDialog(
        title: l10n.get('draftFound'),
        content: l10n.get('draftRestorePrompt'),
        leftBtn: TDDialogButtonOptions(
          title: l10n.get('discard'),
          titleColor: colors.textSecondary,
          action: () {
            Navigator.pop(ctx);
            ExpenseCreateController.deleteDraft();
          },
        ),
        rightBtn: TDDialogButtonOptions(
          title: l10n.get('restore'),
          titleColor: colors.primary,
          action: () async {
            Navigator.pop(ctx);
            final draft = await ExpenseCreateController.loadDraft();
            if (draft != null && mounted) {
              final api = ref.read(expenseApiProvider);
              ref
                  .read(expenseCreateProvider(widget.editId).notifier)
                  .restoreFromDraft(draft, api);
              _purposeController.text = draft.purpose;
              _remarkController.text = draft.remark;
              if (draft.attachments.isNotEmpty) {
                await _attachmentController.restoreFromPaths(draft.attachments);
              }
            }
          },
        ),
      ),
    );
  }

  String _currencyLabel(String code) {
    final match = _currencies.where((c) => c.curId == code);
    return match.isNotEmpty ? '${match.first.curId}/${match.first.name}' : code;
  }

  List<Map<String, dynamic>> _convertAcctTree(dynamic tree) {
    if (tree is! List) return [];
    return (tree as List).map<Map<String, dynamic>>((item) {
      final map = Map<String, dynamic>.from(item is Map ? item : {});
      if (map.containsKey('children') && map['children'] != null) {
        map['children'] = _convertAcctTree(map['children']);
      }
      return map;
    }).toList();
  }

  void _autoSelectDept() {
    if (_selectedDeptId.isNotEmpty) return;
    final dep = HostAppChannel.dep;
    if (dep.isEmpty) return;
    final match = _departments.where((d) => d.dep == dep);
    if (match.isNotEmpty) {
      _selectedDeptId = match.first.dep;
      _selectedDeptName = match.first.name;
    }
  }

  void _autoSelectEmployee() {
    if (_selEmployee != null) return;
    final usr = HostAppChannel.usr;
    if (usr.isEmpty || _employees.isEmpty) return;
    final match = _employees.where((e) => e.salNo == usr);
    if (match.isNotEmpty) {
      setState(() => _selEmployee = match.first);
    }
  }

  Future<void> _showEmployeePicker() async {
    FocusManager.instance.primaryFocus?.unfocus();
    final l10n = AppLocalizations.of(context);
    final api = ref.read(expenseApiProvider);
    final result = await showSearchablePicker<EmployeeItem>(
      context,
      title: '${l10n.get('select')}${l10n.get('expensePersonnel')}',
      searchHint: l10n.get('search'),
      loader: (keyword, page) =>
          api.getEmployees(keyword: keyword, page: page, size: 20),
      labelBuilder: (e) => e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}',
      onRefresh: () => api.clearRefCache(),
    );
    if (result != null && mounted) {
      setState(() => _selEmployee = result);
    }
  }

  Future<void> _showDeptPicker() async {
    FocusManager.instance.primaryFocus?.unfocus();
    final l10n = AppLocalizations.of(context);
    final api = ref.read(expenseApiProvider);
    final result = await showSearchablePicker<DepartmentItem>(
      context,
      title: '${l10n.get('select')}${l10n.get('expenseDept')}',
      searchHint: l10n.get('search'),
      loader: (keyword, page) =>
          api.getDepartments(keyword: keyword, page: page, size: 20),
      labelBuilder: (d) => d.name.isEmpty ? d.dep : '${d.dep} ${d.name}',
      onRefresh: () => api.clearRefCache(),
    );
    if (result != null && mounted) {
      setState(() {
        _selectedDeptId = result.dep;
        _selectedDeptName = result.name;
      });
    }
  }

  Map<String, dynamic> _buildSubmitData(ExpenseCreateState state) {
    final expense = state.expense;
    return {
      'HeadData': {
        'BX_DD': _today(),
        'DEP': _selectedDeptId,
        'USR_NO': _selEmployee?.salNo ?? '',
        'PAY_ID': expense.paymentMethod,
        //'PRT_SW': 'N',
        'USR': HostAppChannel.usr,
        'REM': expense.remark,
        'CUR_ID': expense.currencyCode,
        'EXC_RTO': 1,
        'REASON': expense.purpose,
        'VOH_ID': expense.isGenerateVoucher ? 'T' : 'F',
      },
      'BodyData1': expense.details.asMap().entries.map((e) {
        final d = e.value;
        return {
          'BX_DD': _today(),
          'ACC_NO': d.acctSubjectId,
          'AMT': d.totalAmount,
          'AMTN': d.amount,
          'AMTN_SH': d.approvedAmount,
          'REM': d.remark,
          'CUST': d.customerVendorId,
          'IDX_NO': d.expenseCategory,
          'OBJ_NO': d.projectId.isNotEmpty ? d.projectId : '',
          'TAX': d.taxAmount,
          'TAX_RTO': d.taxRate,
          'DEP': d.costDeptId,
          'AE_NO': d.aeNo,
          'AE_DD': d.aeDd,
          'BNK_NO': d.bankName,
          'BNK_ID': d.bankAccount,
          'ACCNAME': d.bankAccountName,
          'SQ_MAN': d.sqMan.isNotEmpty ? d.sqMan : HostAppChannel.usr,
          'EST_ITM': d.aeNo.isNotEmpty ? d.sortOrder : 0,
        };
      }).toList(),
    };
  }

  Widget _buildImportLink() {
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final l10n = AppLocalizations.of(context);
    return GestureDetector(
      onTap: () async {
        final result = await GoRouter.of(
          context,
        ).push<List<ImportableItem>>('/expense/import-apply');
        if (result == null || result.isEmpty || !mounted) return;
        // 将选中的导入数据转换为明细，先移除同单号的旧数据
        final controller = ref.read(
          expenseCreateProvider(widget.editId).notifier,
        );
        final aeNos = result.map((e) => e.aeNo).toSet();
        for (final aeNo in aeNos) {
          controller.removeDetailsByAeNo(aeNo);
        }
        // 拼接申请事由到报销单头 purpose（同张单多次转入只拼一次）
        final newReasons = <String>[];
        for (final aeNo in aeNos) {
          if (!_importedAeNos.contains(aeNo)) {
            final item = result.firstWhere((e) => e.aeNo == aeNo);
            if (item.reason.isNotEmpty) {
              newReasons.add(item.reason);
              _importedAeNos.add(aeNo);
            }
          }
        }
        if (newReasons.isNotEmpty) {
          final currentPurpose = controller.currentState.expense.purpose;
          final joined = newReasons.join('；');
          final newPurpose = currentPurpose.isNotEmpty
              ? '$currentPurpose；$joined'
              : joined;
          controller.updatePurpose(newPurpose);
          _purposeController.text = newPurpose;
        }
        final now = DateTime.now();
        for (final item in result) {
          controller.addDetail(
            ExpenseDetailModel(
              id: '${now.millisecondsSinceEpoch}_${item.itm}',
              expenseId: '',
              expenseApplyId: '',
              expenseApplyNo: item.aeNo,
              expenseApplyDate: item.aeDd.isNotEmpty
                  ? DateTime.tryParse(item.aeDd)
                  : null,
              expenseCategory: item.typeNo,
              categoryName: item.typeName,
              purpose: item.rem,
              priority: item.priority,
              projectId: item.objNo,
              projectName: item.objName,
              costDeptId: item.dep,
              costDeptName: item.depName,
              acctSubjectId: item.accNo,
              acctSubjectName: item.accName,
              amount: item.amtnYj,
              taxRate: 0,
              taxAmount: 0,
              totalAmount: item.amtnYj,
              currencyCode: '',
              exchangeRate: 1.0,
              baseAmount: item.amtnYj,
              approvedAmount: item.amtnYj,
              customerVendorId: '',
              customerVendorName: '',
              bankName: '',
              bankAccountName: '',
              bankAccount: '',
              remark: item.rem,
              sortOrder: item.preItm ?? item.itm,
              attachments: const [],
              sqMan: item.sqMan,
              sqManName: item.sqName,
              aeNo: item.aeNo,
              aeDd: item.aeDd,
              createTime: now,
              updateTime: now,
            ),
          );
        }
        controller.recalculateAmount();
        TDToast.showSuccess(l10n.get('importSuccess'), context: context);
        _scrollToDetailSection();
      },
      child: Container(
        height: 44,
        decoration: BoxDecoration(
          color: colors.primaryLight,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.download, size: 14, color: colors.primary),
            const SizedBox(width: 8),
            Text(
              l10n.get('importApprovedPreApp'),
              style: TextStyle(
                fontSize: AppFontSizes.body,
                color: colors.primary,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBasicInfoSection(
    ExpenseCreateController controller,
    ExpenseCreateState state,
  ) {
    final l10n = AppLocalizations.of(context);
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final expense = state.expense;
    return FormSection(
      title: l10n.get('basicInfo'),
      leadingIcon: Icons.info_outline,
      children: [
        FormFieldRow(
          label: l10n.get('date'),
          value: _today(),
          readOnly: true,
          showArrow: false,
        ),
        const SizedBox(height: 16),
        FormFieldRow(
          label: l10n.get('expensePersonnel'),
          value: _selEmployee != null
              ? '${_selEmployee!.salNo}/${_selEmployee!.name}'
              : '',
          hint: l10n.get('pleaseSelect'),
          onTap: _employees.isNotEmpty ? _showEmployeePicker : null,
        ),
        const SizedBox(height: 16),
        FormFieldRow(
          label: l10n.get('expenseDept'),
          value: _selectedDeptName.isNotEmpty
              ? '$_selectedDeptId/$_selectedDeptName'
              : null,
          hint: l10n.get('pleaseSelect'),
          onTap: _refDataLoading ? null : () => _showDeptPicker(),
        ),
        const SizedBox(height: 16),
        _label(l10n.get('expenseReason'), required: true),
        const SizedBox(height: 8),
        TDTextarea(
          controller: _purposeController,
          focusNode: _purposeFocus,
          hintText: l10n.get('enterExpenseReason'),
          maxLines: 4,
          minLines: 1,
          maxLength: 500,
          indicator: true,
          padding: EdgeInsets.zero,
          bordered: true,
          backgroundColor: colors.bgPage,
          onChanged: (_) => controller.updatePurpose(_purposeController.text),
        ),
        const SizedBox(height: 16),
        FormFieldRow(
          label: l10n.get('paymentMethod'),
          value: expense.paymentMethod,
          hint: l10n.get('pleaseEnter'),
          onTap: () => _showTextInput(
            l10n.get('paymentMethod'),
            (v) => controller.updatePaymentMethod(v),
            initialText: expense.paymentMethod,
          ),
          onClear: () => controller.updatePaymentMethod(''),
        ),
        const SizedBox(height: 16),
        FormFieldRow(
          label: l10n.get('currency'),
          value: expense.currencyCode.isNotEmpty
              ? _currencyLabel(expense.currencyCode)
              : null,
          hint: l10n.get('selectCurrency'),
          onTap: () => _showCurrencyPicker(controller, expense.currencyCode),
          onClear: () => controller.updateCurrencyCode(''),
        ),
        const SizedBox(height: 16),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              l10n.get('generateVoucher'),
              style: TextStyle(
                fontSize: AppFontSizes.subtitle,
                color: colors.textSecondary,
              ),
            ),
            TDSwitch(
              isOn: expense.isGenerateVoucher,
              onChanged: (v) {
                controller.setGenerateVoucher(v);
                return v;
              },
            ),
          ],
        ),
        const SizedBox(height: 16),
        _label(l10n.get('remark')),
        const SizedBox(height: 8),
        TDTextarea(
          controller: _remarkController,
          focusNode: _remarkFocus,
          hintText: l10n.get('enterRemark'),
          maxLines: 3,
          minLines: 1,
          maxLength: 500,
          indicator: true,
          padding: EdgeInsets.zero,
          bordered: true,
          backgroundColor: colors.bgPage,
          onChanged: (_) => controller.updateRemark(_remarkController.text),
        ),
      ],
    );
  }

  Widget _buildDetailSection(
    ExpenseCreateController controller,
    ExpenseCreateState state,
  ) {
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final l10n = AppLocalizations.of(context);
    final totalApproved = state.expense.details.fold<double>(
      0,
      (sum, d) => sum + d.approvedAmount,
    );
    return FormSection(
      title: l10n.get('expenseDetails'),
      leadingIcon: Icons.receipt_long_outlined,
      showAction: true,
      actionText: l10n.get('add'),
      onActionTap: () => _showAddDetailDialog(controller),
      children: [
        if (state.expense.details.isEmpty)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Text(
              l10n.get('noDetailHint'),
              style: TextStyle(
                fontSize: AppFontSizes.subtitle,
                color: colors.textPlaceholder,
              ),
            ),
          )
        else
          ...state.expense.details.asMap().entries.map((entry) {
            final d = entry.value;
            return GestureDetector(
              onTap: () =>
                  _showAddDetailDialog(controller, editIndex: entry.key),
              child: Container(
                margin: const EdgeInsets.symmetric(vertical: 6),
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: colors.bgPage,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Row(
                  children: [
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Expanded(
                                child: Text(
                                  d.categoryName.isNotEmpty
                                      ? '${d.expenseCategory}/${d.categoryName}'
                                      : d.expenseCategory,
                                  style: TextStyle(
                                    fontSize: AppFontSizes.body,
                                    fontWeight: FontWeight.w500,
                                    color: colors.textPrimary,
                                  ),
                                ),
                              ),
                              Column(
                                crossAxisAlignment: CrossAxisAlignment.end,
                                children: [
                                  Text(
                                    formatAmount(d.totalAmount),
                                    style: TextStyle(
                                      fontSize: AppFontSizes.body,
                                      fontWeight: FontWeight.w600,
                                      color: colors.amountPrimary,
                                    ),
                                  ),
                                  if (d.approvedAmount > 0)
                                    Text(
                                      formatAmount(d.approvedAmount),
                                      style: TextStyle(
                                        fontSize: AppFontSizes.body,
                                        fontWeight: FontWeight.w600,
                                        color: colors.success,
                                      ),
                                    ),
                                ],
                              ),
                            ],
                          ),
                          const SizedBox(height: 2),
                          Text(
                            '${l10n.get('amountExcludingTax')}: ${formatAmount(d.amount)}',
                            style: TextStyle(
                              fontSize: AppFontSizes.caption,
                              color: colors.textSecondary,
                            ),
                          ),
                          if (d.taxAmount > 0)
                            Text(
                              '${l10n.get('taxAmount')}: ${formatAmount(d.taxAmount)}',
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.taxRate > 0)
                            Text(
                              '${l10n.get('taxRate')}: ${d.taxRate.toStringAsFixed(0)}%',
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.acctSubjectName.isNotEmpty)
                            Text(
                              '${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.aeNo.isNotEmpty)
                            Text(
                              '${l10n.get('expenseApplyNo')}: ${d.aeNo}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.aeDd.isNotEmpty)
                            Text(
                              '${l10n.get('applyDate')}: ${d.aeDd.length >= 10 ? d.aeDd.substring(0, 10) : d.aeDd}',
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.projectName.isNotEmpty)
                            Text(
                              '${l10n.get('project')}: ${d.projectId}/${d.projectName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.costDeptName.isNotEmpty)
                            Text(
                              '${l10n.get('costDept')}: ${d.costDeptId}/${d.costDeptName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.customerVendorName.isNotEmpty)
                            Text(
                              '${l10n.get('customerVendor')}: ${d.customerVendorId}/${d.customerVendorName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.sqManName.isNotEmpty)
                            Text(
                              '${l10n.get('applicant')}: ${d.sqMan}/${d.sqManName}',
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.bankAccountName.isNotEmpty)
                            Text(
                              '${l10n.get('bankAccountName')}: ${d.bankAccountName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.bankName.isNotEmpty)
                            Text(
                              '${l10n.get('bankName')}: ${d.bankName}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.bankAccount.isNotEmpty)
                            Text(
                              '${l10n.get('bankAccount')}: ${d.bankAccount}',
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                          if (d.remark.isNotEmpty)
                            Text(
                              '${l10n.get('remark')}: ${d.remark}',
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                fontSize: AppFontSizes.caption,
                                color: colors.textSecondary,
                              ),
                            ),
                        ],
                      ),
                    ),
                    const SizedBox(width: 8),
                    GestureDetector(
                      onTap: () {
                        controller.removeDetail(entry.key);
                        controller.recalculateAmount();
                      },
                      child: Icon(
                        Icons.close,
                        size: 18,
                        color: colors.textSecondary,
                      ),
                    ),
                  ],
                ),
              ),
            );
          }),
        const SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              l10n.get('totalExpense'),
              style: TextStyle(
                fontSize: AppFontSizes.body,
                fontWeight: FontWeight.w600,
                color: colors.textPrimary,
              ),
            ),
            Text(
              formatAmount(state.expense.totalAmount),
              style: TextStyle(
                fontSize: AppFontSizes.subtitle,
                fontWeight: FontWeight.w700,
                color: colors.amountPrimary,
              ),
            ),
          ],
        ),
        const SizedBox(height: 4),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              l10n.get('approvedTotal'),
              style: TextStyle(
                fontSize: AppFontSizes.body,
                fontWeight: FontWeight.w600,
                color: colors.textPrimary,
              ),
            ),
            Text(
              formatAmount(totalApproved),
              style: TextStyle(
                fontSize: AppFontSizes.subtitle,
                fontWeight: FontWeight.w700,
                color: totalApproved > 0 ? colors.success : colors.textPrimary,
              ),
            ),
          ],
        ),
      ],
    );
  }

  Future<void> _checkAttachHealth() async {
    if (mounted) setState(() => _attachAvailable = false);
    try {
      final api = ref.read(expenseApiProvider);
      final ok = await api.checkAttachHealth();
      if (mounted) setState(() => _attachAvailable = ok);
    } catch (_) {
      if (mounted) setState(() => _attachAvailable = false);
    }
  }

  Future<void> _checkApprovedAmountRight() async {
    try {
      final api = ref.read(expenseApiProvider);
      final ok = await api.checkSpcRight('MONCJ_AMTN_SH');
      if (mounted) setState(() => _canEditApprovedAmount = ok);
    } catch (_) {
      if (mounted) setState(() => _canEditApprovedAmount = false);
    }
  }

  Future<void> _checkBillFileRights() async {
    try {
      final api = ref.read(expenseApiProvider);
      final rights = await api.getBillFileRights('BX');
      if (mounted) setState(() => _billFileRights = rights);
    } catch (_) {
      if (mounted) setState(() => _billFileRights = BillFileRights.none);
    }
  }

  Widget _buildAttachmentSection(
    ExpenseCreateController controller,
    ExpenseCreateState state,
  ) {
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final l10n = AppLocalizations.of(context);
    final children = <Widget>[];
    if (!_attachAvailable) {
      children.add(
        Text(
          l10n.get('attachServiceUnavailable'),
          style: TextStyle(
            fontSize: AppFontSizes.caption,
            color: colors.textPlaceholder,
          ),
        ),
      );
    } else if (!_billFileRights.canManageAttachments) {
      children.add(
        Text(
          l10n.get('noAttachmentPermission'),
          style: TextStyle(
            fontSize: AppFontSizes.caption,
            color: colors.textPlaceholder,
          ),
        ),
      );
    } else {
      children.addAll([
        Text(
          l10n.get('maxAttachment'),
          style: TextStyle(
            fontSize: AppFontSizes.caption,
            color: colors.textPlaceholder,
          ),
        ),
        const SizedBox(height: 8),
      ]);
    }
    return FormSection(
      title: l10n.get('attachmentUpload'),
      leadingIcon: Icons.attach_file_outlined,
      children: [
        ...children,
        if (_attachAvailable && _billFileRights.canManageAttachments)
          AttachmentPicker(
            controller: _attachmentController,
            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 _buildBottomButtons(
    ExpenseCreateController controller,
    ExpenseCreateState state,
  ) {
    final l10n = AppLocalizations.of(context);
    return ActionBar(
      showLeft: false,
      centerLabel: l10n.get('saveDraft'),
      rightLabel: l10n.get('submit'),
      centerTextOnly: true,
      onCenterTap: () async {
        if (state.isSubmitting) return;
        FocusScope.of(context).unfocus();
        controller.updateAttachments(_attachmentController.toPathList());
        controller.updateDept(_selectedDeptId, _selectedDeptName);
        final ok = await controller.saveDraft();
        if (mounted) {
          if (ok) {
            _forcePop();
          } else {
            TDToast.showFail(l10n.get('saveFailed'), context: context);
          }
        }
      },
      onRightTap: () async {
        if (state.isSubmitting) return;
        final err = _validate(l10n, state);
        if (err.isNotEmpty) {
          TDToast.showText(err.first, context: context);
          return;
        }
        FocusScope.of(context).unfocus();
        LoadingDialog.show(context, text: l10n.get('submitting'));
        try {
          final data = _buildSubmitData(state);
          final api = ref.read(expenseApiProvider);
          final billNo = await api.submit(data);
          // 上传附件（billNo 提取失败则跳过，不影响主流程）
          if (billNo != null) {
            final now = DateTime.now();
            final effDd =
                '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} '
                '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.'
                '${now.millisecond.toString().padLeft(3, '0')}';
            final usr = HostAppChannel.usr;

            // 表头附件
            for (var i = 0; i < _attachmentController.files.length; i++) {
              final file = _attachmentController.files[i];
              try {
                await api.uploadAttachment(file.path, {
                  'BIL_ID': 'BX',
                  'BIL_NO': billNo,
                  'SRCITM': 0,
                  'ITM': i + 1,
                  'TAG': 1,
                  'EFF_DD': effDd,
                  'USR': usr,
                  'FILENAME': file.name,
                  'EXT': file.name.split('.').last,
                });
              } catch (_) {
                // Attachment upload failure is non-fatal
              }
            }

            // 明细附件
            for (var i = 0; i < state.expense.details.length; i++) {
              final detail = state.expense.details[i];
              if (detail.attachments.isEmpty) continue;
              for (var j = 0; j < detail.attachments.length; j++) {
                final file = File(detail.attachments[j]);
                try {
                  if (!await file.exists()) continue;
                  final fileName = file.path.split('/').last;
                  await api.uploadAttachment(file.path, {
                    'BIL_ID': 'BX',
                    'BIL_NO': billNo,
                    'SRCITM': i + 1,
                    'ITM': j + 1,
                    'TAG': 1,
                    'EFF_DD': effDd,
                    'USR': usr,
                    'FILENAME': fileName,
                    'EXT': fileName.split('.').last,
                  });
                } catch (_) {
                  // Detail attachment upload failure is non-fatal
                }
              }
            }
          }
          await ExpenseCreateController.deleteDraft();
          if (mounted) {
            LoadingDialog.hide(context);
            TDToast.showSuccess(
              l10n.get('submitSuccess'),
              context: context,
            );
            GoRouter.of(context).go('/expense/list');
          }
        } catch (e) {
          if (mounted) {
            LoadingDialog.hide(context);
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted) _showSubmitError(e, l10n);
            });
          }
        }
      },
    );
  }

  void _showSubmitError(Object e, AppLocalizations l10n) {
    final message = _extractErrorMessage(e) ?? l10n.get('submitFailedRetry');
    showGeneralDialog(
      context: context,
      pageBuilder: (ctx, animation, secondaryAnimation) => TDConfirmDialog(
        title: l10n.get('submitFailed'),
        content: message,
        buttonStyle: TDDialogButtonStyle.text,
      ),
    );
  }

  String? _extractErrorMessage(Object e) {
    if (e is DioException) {
      if (e.error is ApiException) return (e.error as ApiException).message;
      if (e.error is NetworkException) {
        return (e.error as NetworkException).message;
      }
    }
    return null;
  }

  Future<void> _showAddDetailDialog(
    ExpenseCreateController controller, {
    int? editIndex,
  }) async {
    if (_addingDetail) return;
    _addingDetail = true;
    try {
      final l10n = AppLocalizations.of(context);
      final state = controller.currentState;
      ExpenseDetailInputData? initialData;
      if (editIndex != null) {
        final d = state.expense.details[editIndex];
        initialData = ExpenseDetailInputData(
          category: d.expenseCategory,
          categoryName: d.categoryName,
          acctSubjectId: d.acctSubjectId,
          acctSubjectName: d.acctSubjectName,
          purpose: d.purpose,
          amount: d.totalAmount,
          taxRate: d.taxRate,
          projectId: d.projectId,
          projectName: d.projectName,
          costDeptId: d.costDeptId,
          costDeptName: d.costDeptName,
          customerVendorId: d.customerVendorId,
          customerVendorName: d.customerVendorName,
          approvedAmount: d.approvedAmount,
          bankName: d.bankName,
          bankAccountName: d.bankAccountName,
          bankAccount: d.bankAccount,
          remark: d.remark,
          attachmentPaths: d.attachments,
          sqMan: d.sqMan,
          sqManName: d.sqManName,
          aeNo: d.aeNo,
          aeDd: d.aeDd,
        );
      }
      FocusManager.instance.primaryFocus?.unfocus();
      final result = await ExpenseDetailDialog.show(
        context,
        api: ref.read(expenseApiProvider),
        l10n: l10n,
        initialData: initialData,
        checkAttachHealth: () =>
            ref.read(expenseApiProvider).checkAttachHealth(),
        canEditApprovedAmount: _canEditApprovedAmount,
        billFileRights: _billFileRights,
        acctTree: _acctTree,
      );
      if (result != null && mounted) {
        final now = DateTime.now();
        final detail = ExpenseDetailModel(
          id: editIndex != null
              ? state.expense.details[editIndex].id
              : now.millisecondsSinceEpoch.toString(),
          expenseId: '',
          expenseCategory: result.category,
          categoryName: result.categoryName,
          purpose: result.purpose,
          amount: result.taxRate > 0
              ? result.amount / (1 + result.taxRate / 100)
              : result.amount,
          taxRate: result.taxRate,
          taxAmount: result.taxRate > 0
              ? result.amount - result.amount / (1 + result.taxRate / 100)
              : 0,
          totalAmount: result.amount,
          projectId: result.projectId,
          projectName: result.projectName,
          costDeptId: result.costDeptId,
          costDeptName: result.costDeptName,
          acctSubjectId: result.acctSubjectId,
          acctSubjectName: result.acctSubjectName,
          customerVendorId: result.customerVendorId,
          customerVendorName: result.customerVendorName,
          approvedAmount: result.approvedAmount,
          bankName: result.bankName,
          bankAccountName: result.bankAccountName,
          bankAccount: result.bankAccount,
          sqMan: result.sqMan,
          sqManName: result.sqManName,
          aeNo: result.aeNo,
          aeDd: result.aeDd,
          remark: result.remark,
          sortOrder: editIndex != null
              ? state.expense.details[editIndex].sortOrder
              : 1,
          preItm: editIndex != null
              ? state.expense.details[editIndex].preItm
              : null,
          attachments: result.attachmentPaths,
          createTime: now,
          updateTime: now,
        );
        if (editIndex != null) {
          controller.updateDetail(editIndex, detail);
        } else {
          controller.addDetail(detail);
        }
        controller.recalculateAmount();
      }
    } finally {
      _addingDetail = false;
    }
  }

  void _showCurrencyPicker(ExpenseCreateController controller, String cur) {
    if (_currencies.isEmpty) {
      TDToast.showText(
        AppLocalizations.of(context).get('noData'),
        context: context,
      );
      return;
    }
    final l10n = AppLocalizations.of(context);
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    final codes = _currencies.map((c) => c.curId).toList();
    final labels = _currencies.map((c) => '${c.curId}/${c.name}').toList();
    FocusManager.instance.primaryFocus?.unfocus();
    TDPicker.showMultiPicker(
      context,
      title: l10n.get('selectCurrency'),
      backgroundColor: colors.bgCard,
      data: [labels],
      onConfirm: (s) {
        if (s.isNotEmpty && s[0] is int) {
          final i = s[0] as int;
          if (i >= 0 && i < codes.length) {
            Navigator.of(context).pop();
            controller.updateCurrencyCode(codes[i]);
          }
        }
      },
    );
  }

  void _showTextInput(
    String title,
    Function(String) onConfirm, {
    String initialText = '',
  }) {
    FocusScope.of(context).unfocus();
    FocusManager.instance.primaryFocus?.unfocus();
    final l10n = AppLocalizations.of(context);
    final c = TextEditingController(text: initialText);
    showGeneralDialog(
      context: context,
      pageBuilder: (ctx, animation, secondaryAnimation) => TDInputDialog(
        textEditingController: c,
        title: title,
        hintText: l10n.get('pleaseEnter'),
        leftBtn: TDDialogButtonOptions(
          title: l10n.get('cancel'),
          action: () => Navigator.pop(ctx),
        ),
        rightBtn: TDDialogButtonOptions(
          title: l10n.get('confirm'),
          action: () {
            onConfirm(c.text);
            Navigator.pop(ctx);
          },
        ),
      ),
    );
  }

  Widget _label(String t, {bool required = false}) {
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    return Text.rich(
      TextSpan(
        children: [
          TextSpan(
            text: t,
            style: TextStyle(
              fontSize: AppFontSizes.subtitle,
              color: colors.textSecondary,
            ),
          ),
          if (required)
            TextSpan(
              text: ' *',
              style: TextStyle(
                fontSize: AppFontSizes.subtitle,
                color: colors.danger,
              ),
            ),
        ],
      ),
    );
  }

  List<String> _validate(AppLocalizations l10n, ExpenseCreateState state) {
    final e = <String>[];
    if (_purposeController.text.trim().isEmpty) {
      e.add(l10n.get('enterExpenseReason'));
    }
    if (state.expense.details.isEmpty) {
      e.add(l10n.get('addAtLeastOneDetail'));
    }
    return e;
  }

  bool _hasUnsaved(ExpenseCreateState state) =>
      _purposeController.text.isNotEmpty ||
      state.expense.paymentMethod.isNotEmpty ||
      state.expense.currencyCode.isNotEmpty ||
      _remarkController.text.isNotEmpty ||
      state.expense.details.isNotEmpty ||
      _attachmentController.files.isNotEmpty;

  void _doPop() {
    final l10n = AppLocalizations.of(context);
    final state = ref.read(expenseCreateProvider(widget.editId));
    if (_hasUnsaved(state)) {
      _showConfirmDialog(
        l10n.get('confirmExit'),
        l10n.get('unsavedContentWarning'),
        l10n.get('continueEditing'),
        l10n.get('discardAndExit'),
        () async {
          try {
            await ExpenseCreateController.deleteDraft();
          } catch (_) {}
          if (!mounted) return;
          setState(() {
            _selectedDeptId = '';
            _selectedDeptName = '';
          });
          ref.read(expenseCreateProvider(widget.editId).notifier).reset();
          _forcePop();
        },
      );
    } else {
      _forcePop();
    }
  }

  void _forcePop() {
    FocusManager.instance.primaryFocus?.unfocus();
    final router = GoRouter.of(context);
    if (router.canPop()) {
      router.pop();
    } else {
      SystemNavigator.pop();
    }
  }

  void _showConfirmDialog(
    String title,
    String content,
    String leftText,
    String rightText,
    VoidCallback onConfirm,
  ) {
    FocusScope.of(context).unfocus();
    FocusManager.instance.primaryFocus?.unfocus();
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    showDialog(
      context: context,
      useRootNavigator: true,
      builder: (ctx) => TDAlertDialog(
        title: title,
        content: content,
        buttonStyle: TDDialogButtonStyle.text,
        leftBtn: TDDialogButtonOptions(
          title: leftText,
          titleColor: colors.primary,
          action: () => Navigator.pop(ctx),
        ),
        rightBtn: TDDialogButtonOptions(
          title: rightText,
          titleColor: colors.danger,
          action: () {
            Navigator.pop(ctx);
            onConfirm();
          },
        ),
      ),
    );
  }

  Widget _buildPageFooter() {
    final l10n = AppLocalizations.of(context);
    final colors = Theme.of(context).extension<AppColorsExtension>()!;
    return Center(
      child: Padding(
        padding: const EdgeInsets.only(bottom: 16),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.rocket_launch_outlined,
              size: 16,
              color: colors.textPlaceholder,
            ),
            const SizedBox(width: 6),
            Text(
              l10n.get('pageFooter'),
              style: TextStyle(
                fontSize: AppFontSizes.caption,
                color: colors.textPlaceholder,
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _today() {
    final n = DateTime.now();
    return '${n.year}-${n.month.toString().padLeft(2, '0')}-${n.day.toString().padLeft(2, '0')}';
  }
}
