import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/network/api_client.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/status_badge.dart'; import '../models/derivative_item_model.dart'; import '../repositories/trade_repository.dart'; class DerivativePickerModal extends StatefulWidget { final String underlyingIsin; final String underlyingSymbol; final String underlyingName; final String initialSignalType; final double currentUnderlyingPrice; const DerivativePickerModal({ super.key, required this.underlyingIsin, required this.underlyingSymbol, this.underlyingName = '', this.initialSignalType = 'BUY', this.currentUnderlyingPrice = 0.0, }); static Future show( BuildContext context, { required String underlyingIsin, required String underlyingSymbol, String underlyingName = '', String initialSignalType = 'BUY', double currentUnderlyingPrice = 0.0, }) { return showDialog( context: context, barrierDismissible: true, builder: (ctx) => DerivativePickerModal( underlyingIsin: underlyingIsin, underlyingSymbol: underlyingSymbol, underlyingName: underlyingName, initialSignalType: initialSignalType, currentUnderlyingPrice: currentUnderlyingPrice, ), ); } @override State createState() => _DerivativePickerModalState(); } class _DerivativePickerModalState extends State { late String _optionType; double _targetLeverage = 5.0; final TextEditingController _leverageTextCtrl = TextEditingController(text: '5.0'); String _searchQuery = ''; final TextEditingController _searchCtrl = TextEditingController(); final ScrollController _scrollController = ScrollController(); Timer? _debounceTimer; List _allDerivatives = []; bool _isLoading = true; bool _isLoadingMore = false; bool _hasMoreData = true; int _currentPage = 0; String? _errorMessage; @override void initState() { super.initState(); final isShort = widget.initialSignalType.toUpperCase() == 'SELL' || widget.initialSignalType.toUpperCase() == 'SHORT'; _optionType = isShort ? 'short' : 'long'; _scrollController.addListener(_onScroll); _fetchDerivatives(reset: true); } @override void dispose() { _debounceTimer?.cancel(); _scrollController.removeListener(_onScroll); _scrollController.dispose(); _searchCtrl.dispose(); _leverageTextCtrl.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 250) { if (!_isLoading && !_isLoadingMore && _hasMoreData) { _fetchMoreDerivatives(); } } } String _formatLeverage(double lev) { if (lev <= 0) return '1x'; if (lev == lev.roundToDouble()) { return '${lev.toInt()}x'; } var s = lev.toStringAsFixed(2); if (s.endsWith('0')) { s = s.substring(0, s.length - 1); } return '${s.replaceAll('.', ',')}x'; } String _formatLeverageNum(double lev) { if (lev <= 0) return '1'; if (lev == lev.roundToDouble()) { return lev.toInt().toString(); } var s = lev.toStringAsFixed(2); if (s.endsWith('0')) { s = s.substring(0, s.length - 1); } return s.replaceAll('.', ','); } void _setTargetLeverage(double lev) { setState(() { _targetLeverage = lev; _leverageTextCtrl.text = _formatLeverageNum(lev); }); _fetchDerivatives(reset: true); } Future _fetchDerivatives({bool reset = true, bool forceRefresh = false}) async { if (reset) { setState(() { _isLoading = true; _isLoadingMore = false; _hasMoreData = true; _currentPage = 0; _errorMessage = null; }); } try { final apiClient = context.read(); final repo = TradeRepository(apiClient: apiClient); final isinToUse = widget.underlyingIsin.isNotEmpty ? widget.underlyingIsin : widget.underlyingSymbol; final results = await repo.fetchDerivatives( isinToUse, optionType: _optionType, targetLeverage: _targetLeverage, page: _currentPage, search: _searchQuery.isNotEmpty ? _searchQuery : null, forceRefresh: forceRefresh, ); if (mounted) { setState(() { if (reset) { _allDerivatives = results; } else { // Append and deduplicate by ISIN final existingIsins = _allDerivatives.map((d) => d.isin).toSet(); final newItems = results.where((d) => !existingIsins.contains(d.isin)).toList(); _allDerivatives.addAll(newItems); } if (results.isEmpty || results.length < 50) { _hasMoreData = false; } _currentPage++; _isLoading = false; _isLoadingMore = false; }); if (reset && _scrollController.hasClients) { _scrollController.jumpTo(0); } } } catch (e) { if (mounted) { setState(() { _errorMessage = e.toString(); _isLoading = false; _isLoadingMore = false; }); } } } Future _fetchMoreDerivatives() async { if (_isLoadingMore || !_hasMoreData) return; setState(() { _isLoadingMore = true; }); await _fetchDerivatives(reset: false); } List get _filteredDerivatives { if (_searchQuery.trim().isEmpty) { return _allDerivatives; } final q = _searchQuery.toLowerCase().trim(); return _allDerivatives.where((d) { final matchIsin = d.isin.toLowerCase().contains(q); final matchIssuer = d.issuer.toLowerCase().contains(q) || d.issuerDisplayName.toLowerCase().contains(q); final matchCategory = d.productCategoryName.toLowerCase().contains(q); final matchBarrier = d.barrier.toString().contains(q); return matchIsin || matchIssuer || matchCategory || matchBarrier; }).toList(); } @override Widget build(BuildContext context) { final isLong = _optionType == 'long'; final activeColor = isLong ? AppTheme.primaryEmerald : AppTheme.accentRed; final filtered = _filteredDerivatives; return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: Container( width: 780, constraints: const BoxConstraints(maxHeight: 820), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppTheme.glassBorder), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.6), blurRadius: 30, offset: const Offset(0, 10), ), ], ), child: Column( children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(20, 18, 16, 14), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.accentCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Icon(Icons.bolt, color: AppTheme.accentCyan, size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text( 'Trade Republic Derivate-Finder', style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold), ), const SizedBox(width: 8), if (!_isLoading && _allDerivatives.isNotEmpty) StatusBadge( label: '${_allDerivatives.length} Derivate geladen', color: AppTheme.accentCyan, ), ], ), const SizedBox(height: 3), Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ Text( 'Basiswert: ${widget.underlyingName.isNotEmpty ? widget.underlyingName : (widget.underlyingSymbol.isNotEmpty && widget.underlyingSymbol != widget.underlyingIsin ? widget.underlyingSymbol : widget.underlyingIsin)} (${widget.underlyingIsin})', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), if (widget.currentUnderlyingPrice > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1.5), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.white12), ), child: Text( 'Kurs: €${widget.currentUnderlyingPrice.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold), ), ), ], ), ], ), ), IconButton( onPressed: () => _fetchDerivatives(reset: true, forceRefresh: true), icon: const Icon(Icons.refresh, color: Colors.white70, size: 20), tooltip: 'Neu von Trade Republic abrufen', ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white54), ), ], ), ), const Divider(color: Colors.white12, height: 1), // Controls Bar Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // Direction Selector & Search Row( children: [ // Direction Toggle Container( decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppTheme.glassBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _directionButton('Long (Turbo Bull)', 'long', AppTheme.primaryEmerald), _directionButton('Short (Turbo Bear)', 'short', AppTheme.accentRed), ], ), ), const SizedBox(width: 12), // Search Input Expanded( child: TextField( controller: _searchCtrl, onChanged: (val) => setState(() { _searchQuery = val; }), style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( hintText: 'Emittent (HSBC, SocGen...), Barriere, ISIN...', hintStyle: TextStyle(color: AppTheme.textMuted, fontSize: 12), prefixIcon: const Icon(Icons.search, size: 16, color: Colors.white54), filled: true, fillColor: AppTheme.glassSurface, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)), ), ), ), ], ), const SizedBox(height: 12), // Interactive Target-Leverage Control Unit Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.glassBorder), ), child: Row( children: [ Text( 'Wunsch-Hebel:', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(width: 10), // Minus button _stepButton(Icons.remove, () { final cur = double.tryParse(_leverageTextCtrl.text.replaceAll(',', '.')) ?? _targetLeverage; final next = (cur - 1.0 < 1.0) ? 1.0 : (cur - 1.0); _setTargetLeverage(next); }, activeColor), const SizedBox(width: 6), // Numeric input SizedBox( width: 68, height: 34, child: TextField( controller: _leverageTextCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 6), filled: true, fillColor: Colors.black.withValues(alpha: 0.35), suffixText: 'x', suffixStyle: TextStyle(color: activeColor, fontSize: 12, fontWeight: FontWeight.bold), border: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: activeColor.withValues(alpha: 0.4))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: const BorderSide(color: Colors.white24)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: activeColor, width: 1.5)), ), onChanged: (val) { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 400), () { final parsed = double.tryParse(val.replaceAll(',', '.')); if (parsed != null && parsed > 0) { _targetLeverage = parsed; _fetchDerivatives(reset: true); } }); }, ), ), const SizedBox(width: 6), // Plus button _stepButton(Icons.add, () { final cur = double.tryParse(_leverageTextCtrl.text.replaceAll(',', '.')) ?? _targetLeverage; final next = (cur + 1.0 > 150.0) ? 150.0 : (cur + 1.0); _setTargetLeverage(next); }, activeColor), const SizedBox(width: 14), // Quick Pills Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( children: [1.0, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 50.0, 75.0].map((lev) { final isSelected = (_targetLeverage - lev).abs() < 0.2; return Padding( padding: const EdgeInsets.only(right: 6), child: InkWell( onTap: () => _setTargetLeverage(lev), borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isSelected ? activeColor.withValues(alpha: 0.2) : Colors.white.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(6), border: Border.all( color: isSelected ? activeColor : Colors.white12, width: isSelected ? 1.5 : 1, ), ), child: Text( '${lev.toInt()}x', style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold, color: isSelected ? activeColor : Colors.white70, ), ), ), ), ); }).toList(), ), ), ), ], ), ), ], ), ), const Divider(color: Colors.white12, height: 1), // Content List (with Streaming Infinite Scroll) Expanded( child: _buildListContent(filtered, activeColor), ), // Streaming Status Footer if (!_isLoading && filtered.isNotEmpty) ...[ const Divider(color: Colors.white12, height: 1), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${filtered.length} Derivate ab ${_formatLeverage(_targetLeverage)} Hebel', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), Row( children: [ if (_hasMoreData) ...[ Icon(Icons.arrow_downward, size: 14, color: AppTheme.accentCyan), const SizedBox(width: 4), Text( 'Scrolle nach unten für höhere Hebel', style: TextStyle(color: AppTheme.accentCyan, fontSize: 11, fontWeight: FontWeight.bold), ), ] else ...[ Icon(Icons.check_circle, size: 14, color: AppTheme.primaryEmerald), const SizedBox(width: 4), Text( 'Alle Derivate dieser Spanne geladen', style: TextStyle(color: AppTheme.primaryEmerald, fontSize: 11, fontWeight: FontWeight.bold), ), ], ], ), ], ), ), ], ], ), ), ); } Widget _stepButton(IconData icon, VoidCallback onPressed, Color activeColor) { return InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.white12), ), child: Icon(icon, size: 16, color: Colors.white), ), ); } Widget _directionButton(String label, String value, Color color) { final isSelected = _optionType == value; return GestureDetector( onTap: () { if (_optionType != value) { setState(() { _optionType = value; }); _fetchDerivatives(reset: true); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isSelected ? color.withValues(alpha: 0.2) : Colors.transparent, borderRadius: BorderRadius.circular(8), border: isSelected ? Border.all(color: color.withValues(alpha: 0.6)) : null, ), child: Text( label, style: TextStyle( color: isSelected ? color : AppTheme.textMuted, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 12, ), ), ), ); } Widget _buildListContent(List filtered, Color activeColor) { if (_isLoading) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppTheme.accentCyan), const SizedBox(height: 12), Text('Lade Live-Derivate ab ${_formatLeverage(_targetLeverage)} Hebel...', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)), ], ), ); } if (_errorMessage != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 36, color: AppTheme.accentRed), const SizedBox(height: 8), Text('Fehler beim Laden der Derivate', style: TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(_errorMessage!, style: TextStyle(color: AppTheme.textMuted, fontSize: 12), textAlign: TextAlign.center), const SizedBox(height: 12), ElevatedButton.icon( onPressed: () => _fetchDerivatives(reset: true, forceRefresh: true), icon: const Icon(Icons.refresh, size: 16), label: const Text('Erneut versuchen'), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black), ), ], ), ), ); } if (filtered.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.search_off, size: 36, color: AppTheme.textMuted), const SizedBox(height: 8), Text('Keine Derivate für diesen Hebel gefunden', style: TextStyle(color: AppTheme.textSecondary, fontSize: 14, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text('Wähle einen anderen Hebel (z. B. 2x, 5x, 10x) oder leere die Suche.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)), const SizedBox(height: 12), TextButton( onPressed: () { _searchCtrl.clear(); _setTargetLeverage(5.0); }, child: Text('Zurücksetzen auf 5x Hebel', style: TextStyle(color: AppTheme.accentCyan)), ), ], ), ); } return ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: filtered.length + (_isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (index == filtered.length) { // Bottom loading indicator when streaming next 50 items return Container( padding: const EdgeInsets.symmetric(vertical: 16), alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.accentCyan), ), const SizedBox(width: 10), Text('Lade nächste 50 Derivate von Trade Republic...', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)), ], ), ); } final item = filtered[index]; final dist = widget.currentUnderlyingPrice > 0 ? item.distanceToBarrier(widget.currentUnderlyingPrice) : 0.0; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.glassBorder), ), child: Row( children: [ // Leverage Badge Container( width: 58, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: activeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), border: Border.all(color: activeColor.withValues(alpha: 0.4)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _formatLeverage(item.leverage), style: TextStyle(color: activeColor, fontWeight: FontWeight.bold, fontSize: 14), ), Text( 'HEBEL', style: TextStyle(color: activeColor.withValues(alpha: 0.8), fontSize: 9, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(width: 12), // Derivative Details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( item.issuerDisplayName.isNotEmpty ? item.issuerDisplayName : item.issuer, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1.5), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(4), ), child: Text( item.nextGenProductCategoryName.isNotEmpty ? item.nextGenProductCategoryName : item.productCategoryName, style: TextStyle(color: AppTheme.textSecondary, fontSize: 10, fontWeight: FontWeight.w600), ), ), ], ), const SizedBox(height: 4), Row( children: [ Text('ISIN: ${item.isin}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), const SizedBox(width: 10), if (item.barrier > 0) Text( 'KO: €${item.barrier.toStringAsFixed(2)}', style: const TextStyle(color: Colors.orangeAccent, fontSize: 11, fontWeight: FontWeight.bold), ), if (dist > 0) ...[ const SizedBox(width: 6), Text( '(${dist.toStringAsFixed(1)}% Abst.)', style: TextStyle(color: dist < 5.0 ? AppTheme.accentRed : AppTheme.textMuted, fontSize: 11), ), ], ], ), ], ), ), // Action Select Button ElevatedButton( onPressed: () { Navigator.of(context).pop(item); }, style: ElevatedButton.styleFrom( backgroundColor: activeColor, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Wählen', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), ), ], ), ); }, ); } }