import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart' hide Path;
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx;
import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude;
import 'package:waveform_recorder/waveform_recorder.dart';

import '../../../app/router/app_router.dart';
import '../../../app/theme/mapflow_theme.dart';
import '../../../shared/auth/telegram_login_button.dart';
import '../../../shared/auth/telegram_session.dart' as telegram_session;
import '../../../shared/runtime_config/runtime_config.dart' as runtime_config;
import '../application/place_cubit.dart';
import '../data/auth_repository.dart';
import '../data/places_repository.dart';
import '../domain/place_models.dart';
import 'widgets/place_photo_card.dart';

String get _mapboxStyle {
  final style = runtime_config.mapboxStyle().trim();
  if (style.isEmpty ||
      style == 'mapbox/standard' ||
      style == mbx.MapboxStyles.STANDARD) {
    return mbx.MapboxStyles.STANDARD_SATELLITE;
  }
  return style;
}

class MapflowShell extends StatelessWidget {
  const MapflowShell({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PlaceCubit, PlaceViewState>(
      builder: (context, viewState) {
        return switch (viewState.status) {
          PlaceLoadStatus.loading => const _MapLoading(),
          PlaceLoadStatus.failure => _MapError(
            message: viewState.errorMessage ?? '',
          ),
          PlaceLoadStatus.ready =>
            viewState.placeState!.hasTelegramAuth
                ? _MapContent(state: viewState.placeState!)
                : _TelegramLoginScreen(
                    onAuthenticated: () => context.read<PlaceCubit>().load(),
                  ),
        };
      },
    );
  }
}

class _MapContent extends StatefulWidget {
  const _MapContent({required this.state});

  final PlaceState state;

  @override
  State<_MapContent> createState() => _MapContentState();
}

class _MapContentState extends State<_MapContent> {
  @override
  Widget build(BuildContext context) {
    final state = widget.state;
    final selected = state.selectedPlace;
    final userCoordinate = state.userCoordinate;
    final targetCenter = _focusCenter(state);
    final placeCubit = context.read<PlaceCubit>();
    final traitCounts = _countPlaceTraits(state.places);
    final availableTraits = [
      for (final trait in PlaceTrait.values)
        if ((traitCounts[trait] ?? 0) > 0) trait,
    ];
    final hasVoiceRecommendation =
        state.voiceFilterTranscript.isNotEmpty ||
        state.voiceFilterTags.isNotEmpty;
    final hasRecommendationSurface =
        hasVoiceRecommendation || state.selectedTrait != null;
    final activeRecommendationLabel = hasVoiceRecommendation
        ? 'Голосовой поиск'
        : state.selectedTrait?.label;

    return Scaffold(
      body: Stack(
        children: [
          _MapboxMapLayer(
            focusCenter: targetCenter,
            places: state.recommendations,
            selectedPlaceId: selected?.id,
            userCoordinate: userCoordinate,
            onPlaceTap: (place) => _selectAndShowPlace(context, place),
          ),
          if (targetCenter == null) const _MapTargetLoadingOverlay(),
          SafeArea(
            child: Align(
              alignment: Alignment.topLeft,
              child: _UserAvatar(
                user: state.currentUser,
                onAdminReviews: state.currentUser?.isAdmin == true
                    ? () => context.push(MapflowRoutes.adminReviews)
                    : null,
                onLogout: () {
                  telegram_session.clearMapflowSession();
                  telegram_session.reloadApp();
                },
              ),
            ),
          ),
          SafeArea(
            child: Align(
              alignment: Alignment.topRight,
              child: _AddReviewAction(
                onPressed: () => _openAddFlow(
                  context,
                  userCoordinate ?? selected?.coordinate,
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: SafeArea(
              top: false,
              child: hasRecommendationSurface
                  ? Padding(
                      padding: const EdgeInsets.only(bottom: 70),
                      child: _PlaceCarousel(
                        places: state.recommendations,
                        onSelect: (place) =>
                            _selectAndShowPlace(context, place),
                        onFavoriteToggle: placeCubit.toggleFavorite,
                      ),
                    )
                  : const SizedBox.shrink(),
            ),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: SafeArea(
              top: false,
              child: _MapBottomControls(
                activeRecommendationLabel: activeRecommendationLabel,
                onClearRecommendation: activeRecommendationLabel == null
                    ? null
                    : hasVoiceRecommendation
                    ? () => unawaited(placeCubit.clearVoiceFilter())
                    : placeCubit.clearTrait,
                onSearch: () => _openSearchSheet(
                  context,
                  traits: availableTraits,
                  selectedTrait: state.selectedTrait,
                  traitCounts: traitCounts,
                  onVoice: placeCubit.recommendPlacesByVoice,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  LatLng? _focusCenter(PlaceState state) {
    return state.userCoordinate ?? state.selectedPlace?.coordinate;
  }

  Future<void> _openSearchSheet(
    BuildContext context, {
    required List<PlaceTrait> traits,
    required PlaceTrait? selectedTrait,
    required Map<PlaceTrait, int> traitCounts,
    required Future<void> Function({
      required String audioContentBase64,
      required String audioMimeType,
    })
    onVoice,
  }) async {
    await showModalBottomSheet<void>(
      context: context,
      showDragHandle: true,
      builder: (sheetContext) {
        return _SearchSheet(
          traits: traits,
          selectedTrait: selectedTrait,
          traitCounts: traitCounts,
          onVoice: onVoice,
          onSelectTrait: (trait) {
            final controller = context.read<PlaceCubit>();
            if (trait == selectedTrait) {
              controller.clearTrait();
            } else {
              controller.selectTrait(trait);
            }
            Navigator.of(sheetContext).pop();
          },
        );
      },
    );
  }

  void _openAddFlow(BuildContext context, LatLng? coordinate) {
    context.push(MapflowRoutes.addExperienceLocation(coordinate: coordinate));
  }

  void _selectAndShowPlace(BuildContext context, PlaceRecommendation place) {
    final placeCubit = context.read<PlaceCubit>()..selectPlace(place.id);
    final placeDetails = placeCubit.loadPlaceDetails(place);
    unawaited(
      showModalBottomSheet<void>(
        context: context,
        isScrollControlled: true,
        isDismissible: true,
        enableDrag: true,
        showDragHandle: true,
        builder: (sheetContext) {
          return DraggableScrollableSheet(
            expand: false,
            initialChildSize: 0.64,
            minChildSize: 0.24,
            maxChildSize: 0.9,
            builder: (_, scrollController) {
              return _PlaceDetailsSheet(
                place: place,
                details: placeDetails,
                scrollController: scrollController,
                onFavoriteToggle: (selectedPlace) =>
                    placeCubit.toggleFavorite(selectedPlace),
                onPhotoTap: (photoUrls, index) =>
                    _openPhotoViewer(sheetContext, photoUrls, index),
              );
            },
          );
        },
      ),
    );
  }

  void _openPhotoViewer(
    BuildContext context,
    List<String> photoUrls,
    int initialIndex,
  ) {
    if (photoUrls.isEmpty) {
      return;
    }

    unawaited(
      showDialog<void>(
        context: context,
        barrierColor: Colors.black,
        builder: (_) {
          return _PlacePhotoViewer(
            photoUrls: photoUrls,
            initialIndex: initialIndex,
          );
        },
      ),
    );
  }
}

Map<PlaceTrait, int> _countPlaceTraits(List<PlaceRecommendation> places) {
  final counts = <PlaceTrait, int>{};
  for (final place in places) {
    for (final trait in place.traits) {
      counts[trait] = (counts[trait] ?? 0) + 1;
    }
  }
  return counts;
}

class _UserLocationMarker extends StatelessWidget {
  const _UserLocationMarker();

  @override
  Widget build(BuildContext context) {
    final color = Theme.of(context).colorScheme.primary;
    return DecoratedBox(
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: color.withValues(alpha: 0.18),
      ),
      child: Center(
        child: Container(
          width: 14,
          height: 14,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: color,
            border: Border.all(color: context.mapflowTokens.mapPanel, width: 3),
          ),
        ),
      ),
    );
  }
}

class _ProjectedMarker {
  const _ProjectedMarker({required this.place, required this.offset});

  final PlaceRecommendation place;
  final Offset offset;
}

class _MapboxMapLayer extends StatefulWidget {
  const _MapboxMapLayer({
    required this.focusCenter,
    required this.places,
    required this.selectedPlaceId,
    required this.userCoordinate,
    required this.onPlaceTap,
  });

  final LatLng? focusCenter;
  final List<PlaceRecommendation> places;
  final String? selectedPlaceId;
  final LatLng? userCoordinate;
  final ValueChanged<PlaceRecommendation> onPlaceTap;

  @override
  State<_MapboxMapLayer> createState() => _MapboxMapLayerState();
}

class _MapboxMapLayerState extends State<_MapboxMapLayer> {
  static const _worldCenter = LatLng(0, 0);

  mbx.MapboxMap? _mapboxMap;
  var _mapLoaded = false;
  var _didInitialFlyIn = false;
  var _cameraAnimationSequence = 0;
  var _projectionVersion = 0;
  Timer? _initialFlyInTimer;
  LatLng? _lastFocusCenter;
  List<_ProjectedMarker> _projectedPlaces = const [];
  Offset? _projectedUserCoordinate;

  @override
  void didUpdateWidget(covariant _MapboxMapLayer oldWidget) {
    super.didUpdateWidget(oldWidget);
    _updateCameraForFocus();
    unawaited(_projectMarkers());
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        mbx.MapWidget(
          styleUri: _mapboxStyleUri,
          viewport: mbx.CameraViewportState(
            center: _mapboxPoint(_worldCenter),
            zoom: 0.48,
            bearing: 0,
            pitch: 0,
          ),
          onMapCreated: _onMapCreated,
          onStyleLoadedListener: (_) {
            if (!kIsWeb) {
              unawaited(_enableGlobeProjection());
            }
          },
          onMapLoadedListener: (_) {
            _mapLoaded = true;
            _updateCameraForFocus();
            unawaited(_projectMarkers());
          },
          onCameraChangeListener: (_) => unawaited(_projectMarkers()),
          onMapIdleListener: (_) => unawaited(_projectMarkers()),
        ),
        for (final projected in _projectedPlaces)
          Positioned(
            left: projected.offset.dx - 26,
            top: projected.offset.dy - 26,
            width: 52,
            height: 52,
            child: _PlaceMarker(
              selected: projected.place.id == widget.selectedPlaceId,
              onTap: () => widget.onPlaceTap(projected.place),
            ),
          ),
        if (_projectedUserCoordinate != null)
          Positioned(
            left: _projectedUserCoordinate!.dx - 15,
            top: _projectedUserCoordinate!.dy - 15,
            width: 30,
            height: 30,
            child: const _UserLocationMarker(),
          ),
      ],
    );
  }

  void _onMapCreated(mbx.MapboxMap mapboxMap) {
    _mapboxMap = mapboxMap;
  }

  @override
  void dispose() {
    _initialFlyInTimer?.cancel();
    super.dispose();
  }

  Future<void> _enableGlobeProjection() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) {
      return;
    }
    await mapboxMap.style.setProjection(
      mbx.StyleProjection(name: mbx.StyleProjectionName.globe),
    );
  }

  void _updateCameraForFocus() {
    final mapboxMap = _mapboxMap;
    final focusCenter = widget.focusCenter;
    if (!_mapLoaded || mapboxMap == null || focusCenter == null) {
      return;
    }
    if (focusCenter == _lastFocusCenter) {
      return;
    }

    _lastFocusCenter = focusCenter;
    final camera = mbx.CameraOptions(
      center: _mapboxPoint(focusCenter),
      zoom: 15.0,
      bearing: -12,
      pitch: 60,
    );
    if (_didInitialFlyIn) {
      _cameraAnimationSequence++;
      unawaited(
        mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 650)),
      );
      return;
    }

    _didInitialFlyIn = true;
    final animationSequence = ++_cameraAnimationSequence;
    unawaited(
      mapboxMap.easeTo(
        mbx.CameraOptions(
          center: _mapboxPoint(_targetOrbitCenter(focusCenter)),
          zoom: 0.72,
          bearing: _targetOrbitBearing(focusCenter),
          pitch: 0,
        ),
        mbx.MapAnimationOptions(duration: 850),
      ),
    );
    _initialFlyInTimer?.cancel();
    _initialFlyInTimer = Timer(const Duration(milliseconds: 520), () {
      if (!mounted ||
          animationSequence != _cameraAnimationSequence ||
          _mapboxMap != mapboxMap) {
        return;
      }
      unawaited(
        mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 2800)),
      );
    });
  }

  LatLng _targetOrbitCenter(LatLng target) {
    final latitude = (target.latitude * 0.18).clamp(-18.0, 18.0);
    return LatLng(latitude, target.longitude);
  }

  double _targetOrbitBearing(LatLng target) {
    return (-target.longitude * 0.34).clamp(-64.0, 64.0);
  }

  Future<void> _projectMarkers() async {
    final mapboxMap = _mapboxMap;
    if (!_mapLoaded || mapboxMap == null) {
      return;
    }

    final version = ++_projectionVersion;
    final coordinates = [
      for (final place in widget.places) _mapboxPoint(place.coordinate),
      if (widget.userCoordinate != null) _mapboxPoint(widget.userCoordinate!),
    ];
    if (coordinates.isEmpty) {
      if (!mounted) {
        return;
      }
      setState(() {
        _projectedPlaces = const [];
        _projectedUserCoordinate = null;
      });
      return;
    }

    final pixels = await mapboxMap.pixelsForCoordinates(coordinates);
    if (!mounted || version != _projectionVersion) {
      return;
    }

    setState(() {
      _projectedPlaces = [
        for (var index = 0; index < widget.places.length; index++)
          _ProjectedMarker(
            place: widget.places[index],
            offset: Offset(pixels[index].x, pixels[index].y),
          ),
      ];
      _projectedUserCoordinate = widget.userCoordinate == null
          ? null
          : Offset(pixels.last.x, pixels.last.y);
    });
  }
}

class _UserAvatar extends StatelessWidget {
  const _UserAvatar({
    required this.user,
    required this.onAdminReviews,
    required this.onLogout,
  });

  final AppUser? user;
  final VoidCallback? onAdminReviews;
  final VoidCallback onLogout;

  @override
  Widget build(BuildContext context) {
    final photoUrl = user?.photoUrl;
    final imageUrl = photoUrl == null || photoUrl.isEmpty
        ? null
        : _avatarImageUrl(photoUrl);
    final fallback = _fallbackText();

    return Padding(
      padding: const EdgeInsets.only(left: 12, top: 8),
      child: PopupMenuButton<_AvatarAction>(
        tooltip: '',
        offset: const Offset(0, 8),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        onSelected: (action) {
          switch (action) {
            case _AvatarAction.adminReviews:
              onAdminReviews?.call();
            case _AvatarAction.logout:
              onLogout();
          }
        },
        itemBuilder: (_) => [
          if (onAdminReviews != null)
            const PopupMenuItem<_AvatarAction>(
              value: _AvatarAction.adminReviews,
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(Icons.table_rows_outlined, size: 18),
                  SizedBox(width: 10),
                  Text('Отзывы'),
                ],
              ),
            ),
          const PopupMenuItem<_AvatarAction>(
            value: _AvatarAction.logout,
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.logout, size: 18),
                SizedBox(width: 10),
                Text('Выйти'),
              ],
            ),
          ),
        ],
        child: ClipOval(
          child: SizedBox.square(
            dimension: 44,
            child: imageUrl == null
                ? _AvatarFallback(text: fallback)
                : _AvatarImage(url: imageUrl, fallback: fallback),
          ),
        ),
      ),
    );
  }

  String _fallbackText() {
    final firstName = user?.firstName?.trim();
    if (firstName != null && firstName.isNotEmpty) {
      return firstName.characters.first.toUpperCase();
    }

    final username = user?.username?.trim();
    if (username != null && username.isNotEmpty) {
      return username.characters.first.toUpperCase();
    }

    return 'M';
  }

  String _avatarImageUrl(String photoUrl) {
    final separator = photoUrl.contains('?') ? '&' : '?';
    return '$photoUrl${separator}v=2';
  }
}

class _AddReviewAction extends StatelessWidget {
  const _AddReviewAction({required this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final colorScheme = Theme.of(context).colorScheme;

    return Padding(
      padding: const EdgeInsets.only(top: 8, right: 12),
      child: FilledButton.icon(
        onPressed: onPressed,
        icon: const Icon(Icons.rate_review_outlined, size: 18),
        label: const Text('Отзыв'),
        style: FilledButton.styleFrom(
          backgroundColor: tokens.mapPanel,
          foregroundColor: colorScheme.onSurface,
          side: BorderSide(color: tokens.mapPanelBorder),
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
        ),
      ),
    );
  }
}

class _MapBottomControls extends StatelessWidget {
  const _MapBottomControls({
    required this.activeRecommendationLabel,
    required this.onClearRecommendation,
    required this.onSearch,
  });

  final String? activeRecommendationLabel;
  final VoidCallback? onClearRecommendation;
  final VoidCallback onSearch;

  @override
  Widget build(BuildContext context) {
    final label = activeRecommendationLabel;

    return Padding(
      padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          _SearchLauncherButton(onPressed: onSearch),
          if (label != null && onClearRecommendation != null) ...[
            const SizedBox(width: 8),
            Flexible(
              child: _ActiveRecommendationChip(
                label: label,
                onClear: onClearRecommendation!,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _ActiveRecommendationChip extends StatelessWidget {
  const _ActiveRecommendationChip({required this.label, required this.onClear});

  final String label;
  final VoidCallback onClear;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return Material(
      color: tokens.mapPanel,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(tokens.panelRadius),
        side: BorderSide(color: tokens.mapPanelBorder),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(12, 6, 4, 6),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.travel_explore_rounded, size: 18),
            const SizedBox(width: 8),
            Flexible(
              child: Text(
                label,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: const TextStyle(fontWeight: FontWeight.w800),
              ),
            ),
            const SizedBox(width: 4),
            IconButton(
              onPressed: onClear,
              icon: const Icon(Icons.close_rounded, size: 18),
              tooltip: 'Сбросить',
              style: IconButton.styleFrom(
                fixedSize: const Size.square(32),
                minimumSize: const Size.square(32),
                padding: EdgeInsets.zero,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _SearchLauncherButton extends StatelessWidget {
  const _SearchLauncherButton({required this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final colorScheme = Theme.of(context).colorScheme;

    return FloatingActionButton.extended(
      heroTag: 'map-search',
      onPressed: onPressed,
      icon: const Icon(Icons.search_rounded),
      label: const Text('Поиск'),
      backgroundColor: tokens.mapPanel,
      foregroundColor: colorScheme.onSurface,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(tokens.panelRadius),
        side: BorderSide(color: tokens.mapPanelBorder),
      ),
    );
  }
}

class _SearchSheet extends StatelessWidget {
  const _SearchSheet({
    required this.traits,
    required this.selectedTrait,
    required this.traitCounts,
    required this.onVoice,
    required this.onSelectTrait,
  });

  final List<PlaceTrait> traits;
  final PlaceTrait? selectedTrait;
  final Map<PlaceTrait, int> traitCounts;
  final Future<void> Function({
    required String audioContentBase64,
    required String audioMimeType,
  })
  onVoice;
  final ValueChanged<PlaceTrait> onSelectTrait;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SizedBox(
        height: 420,
        child: DefaultTabController(
          length: 2,
          child: Column(
            children: [
              const TabBar(
                tabs: [
                  Tab(icon: Icon(Icons.mic_none_rounded), text: 'Голосом'),
                  Tab(icon: Icon(Icons.tune_rounded), text: 'Фильтры'),
                ],
              ),
              Expanded(
                child: TabBarView(
                  children: [
                    _VoiceFilterRecorder(
                      onApply: (audio) async {
                        Navigator.of(context).pop();
                        await onVoice(
                          audioContentBase64: audio.audioContentBase64,
                          audioMimeType: audio.audioMimeType,
                        );
                      },
                    ),
                    _TraitPickerSheet(
                      traits: traits,
                      selectedTrait: selectedTrait,
                      traitCounts: traitCounts,
                      onSelect: onSelectTrait,
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _VoiceFilterAudio {
  const _VoiceFilterAudio({
    required this.audioContentBase64,
    required this.audioMimeType,
  });

  final String audioContentBase64;
  final String audioMimeType;
}

class _VoiceFilterRecorder extends StatefulWidget {
  const _VoiceFilterRecorder({required this.onApply});

  @override
  State<_VoiceFilterRecorder> createState() => _VoiceFilterRecorderState();

  final Future<void> Function(_VoiceFilterAudio audio) onApply;
}

class _VoiceFilterRecorderState extends State<_VoiceFilterRecorder> {
  final _waveController = WaveformRecorderController(
    interval: const Duration(milliseconds: 60),
    config: const RecordConfig(
      numChannels: 1,
      sampleRate: 44100,
      autoGain: true,
      echoCancel: true,
      noiseSuppress: true,
    ),
  );

  var _recording = false;
  var _submitting = false;
  var _hasRecording = false;

  @override
  void dispose() {
    _waveController.dispose();
    super.dispose();
  }

  Future<void> _toggleRecording() async {
    if (_recording) {
      await _waveController.stopRecording();
      setState(() {
        _recording = false;
        _hasRecording = true;
      });
      return;
    }

    await _waveController.startRecording();
    setState(() => _recording = true);
  }

  Future<void> _apply() async {
    if (_recording) {
      await _waveController.stopRecording();
      setState(() {
        _recording = false;
        _hasRecording = true;
      });
    }
    final file = _waveController.file;
    if (file == null) {
      throw StateError('Voice filter recording file is required.');
    }
    setState(() => _submitting = true);
    final bytes = await file.readAsBytes();
    if (!mounted) {
      return;
    }
    await widget.onApply(
      _VoiceFilterAudio(
        audioContentBase64: base64Encode(bytes),
        audioMimeType: file.mimeType ?? 'audio/wav',
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(18, 24, 18, 18),
      child: Column(
        children: [
          Expanded(
            child: Center(
              child: IconButton.filled(
                onPressed: _submitting ? null : _toggleRecording,
                icon: Icon(_recording ? Icons.stop_rounded : Icons.mic_rounded),
                iconSize: 44,
                style: IconButton.styleFrom(
                  fixedSize: const Size.square(112),
                  minimumSize: const Size.square(112),
                ),
                tooltip: _recording ? 'Стоп' : 'Записать',
              ),
            ),
          ),
          FilledButton(
            onPressed: _submitting || !_hasRecording ? null : _apply,
            child: _submitting
                ? const SizedBox.square(
                    dimension: 18,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Применить'),
          ),
        ],
      ),
    );
  }
}

enum _AvatarAction { adminReviews, logout }

class _AvatarImage extends StatelessWidget {
  const _AvatarImage({required this.url, required this.fallback});

  final String url;
  final String fallback;

  @override
  Widget build(BuildContext context) {
    if (url.endsWith('.svg') || url.contains('.svg?')) {
      return SvgPicture.network(
        url,
        fit: BoxFit.cover,
        placeholderBuilder: (_) => _AvatarFallback(text: fallback),
        errorBuilder: (_, _, _) => _AvatarFallback(text: fallback),
      );
    }

    return Image.network(
      url,
      fit: BoxFit.cover,
      errorBuilder: (_, _, _) => _AvatarFallback(text: fallback),
    );
  }
}

class _AvatarFallback extends StatelessWidget {
  const _AvatarFallback({required this.text});

  final String text;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return ColoredBox(
      color: tokens.mapPanel,
      child: Center(
        child: Text(
          text,
          style: TextStyle(
            color: Theme.of(context).colorScheme.onSurface,
            fontWeight: FontWeight.w900,
          ),
        ),
      ),
    );
  }
}

class _TelegramLoginScreen extends StatefulWidget {
  const _TelegramLoginScreen({required this.onAuthenticated});

  final VoidCallback onAuthenticated;

  @override
  State<_TelegramLoginScreen> createState() => _TelegramLoginScreenState();
}

class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
  late final AuthRepository _authRepository;
  var _loading = false;
  var _message = '';

  @override
  void initState() {
    super.initState();
    _authRepository = context.read<AuthRepository>();
    final urlToken = telegram_session.telegramLoginTokenFromUrl();
    if (urlToken.isNotEmpty) {
      _completeLogin(urlToken);
    }
  }

  Future<void> _startLogin() async {
    setState(() {
      _loading = true;
      _message = '';
    });
    final login = await _authRepository.startTelegramBotLogin();
    telegram_session.openExternalUrl(login.botUrl);
    setState(() {
      _loading = false;
      _message = '';
    });
  }

  Future<void> _completeLogin(String token) async {
    setState(() {
      _loading = true;
      _message = '';
    });
    final session = await _authRepository.completeTelegramBotLogin(token);
    telegram_session.saveMapflowSessionToken(session.sessionToken);
    widget.onAuthenticated();
    telegram_session.reloadApp();
  }

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return Scaffold(
      backgroundColor: tokens.darkSurface,
      body: Stack(
        children: [
          const Positioned.fill(child: _AnimatedEarthBackdrop()),
          SafeArea(
            child: Center(
              child: SizedBox(
                width: 320,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(
                      'MapFlow',
                      style: Theme.of(context).textTheme.headlineMedium
                          ?.copyWith(
                            color: tokens.onDark,
                            fontWeight: FontWeight.w900,
                            letterSpacing: 0,
                          ),
                    ),
                    const SizedBox(height: 24),
                    TelegramLoginButton(
                      onPressed: _startLogin,
                      loading: _loading,
                    ),
                    if (_message.isNotEmpty) ...[
                      const SizedBox(height: 14),
                      Text(
                        _message,
                        textAlign: TextAlign.center,
                        style: Theme.of(
                          context,
                        ).textTheme.bodyMedium?.copyWith(color: tokens.onDark),
                      ),
                    ],
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _AnimatedEarthBackdrop extends StatefulWidget {
  const _AnimatedEarthBackdrop();

  @override
  State<_AnimatedEarthBackdrop> createState() => _AnimatedEarthBackdropState();
}

class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 18),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, _) {
          return CustomPaint(
            painter: _EarthBackdropPainter(
              progress: _controller.value,
              tokens: context.mapflowTokens,
            ),
            child: const SizedBox.expand(),
          );
        },
      ),
    );
  }
}

class _EarthBackdropPainter extends CustomPainter {
  const _EarthBackdropPainter({required this.progress, required this.tokens});

  final double progress;
  final MapflowThemeTokens tokens;

  static const _landMasses = <({double lon, double lat, double sx, double sy})>[
    (lon: -106, lat: 42, sx: 0.30, sy: 0.18),
    (lon: -82, lat: 18, sx: 0.20, sy: 0.14),
    (lon: -62, lat: -14, sx: 0.22, sy: 0.25),
    (lon: 16, lat: 8, sx: 0.23, sy: 0.31),
    (lon: 46, lat: 48, sx: 0.34, sy: 0.18),
    (lon: 78, lat: 25, sx: 0.32, sy: 0.20),
    (lon: 116, lat: -23, sx: 0.19, sy: 0.13),
    (lon: 137, lat: 36, sx: 0.10, sy: 0.07),
    (lon: -42, lat: 73, sx: 0.16, sy: 0.08),
  ];

  static const _routes =
      <({double fromLon, double fromLat, double toLon, double toLat})>[
        (fromLon: -74, fromLat: 41, toLon: 2, toLat: 49),
        (fromLon: 2, fromLat: 49, toLon: 105, toLat: 16),
        (fromLon: 139, fromLat: 35, toLon: 105, toLat: 16),
        (fromLon: -122, fromLat: 37, toLon: 139, toLat: 35),
      ];

  @override
  void paint(Canvas canvas, Size size) {
    _paintSpace(canvas, size);
    _paintOrbits(canvas, size);
    _paintEarth(canvas, size);
  }

  void _paintSpace(Canvas canvas, Size size) {
    final rect = Offset.zero & size;
    canvas.drawRect(
      rect,
      Paint()
        ..shader = LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            tokens.darkSurface,
            const Color(0xFF07151B),
            const Color(0xFF150918),
            tokens.darkSurface,
          ],
        ).createShader(rect),
    );

    final glowCenter = Offset(size.width * 0.77, size.height * 0.18);
    canvas.drawCircle(
      glowCenter,
      math.max(size.width, size.height) * 0.48,
      Paint()
        ..shader =
            RadialGradient(
              colors: [
                const Color(0xFFFFB86B).withValues(alpha: 0.20),
                tokens.voiceAccent.withValues(alpha: 0.08),
                Colors.transparent,
              ],
              stops: const [0, 0.34, 1],
            ).createShader(
              Rect.fromCircle(
                center: glowCenter,
                radius: math.max(size.width, size.height) * 0.48,
              ),
            ),
    );

    final stars = Paint()..color = Colors.white;
    for (var index = 0; index < 96; index++) {
      final xSeed = math.sin(index * 12.9898) * 43758.5453;
      final ySeed = math.sin(index * 78.233) * 24634.6345;
      final x = (xSeed - xSeed.floor()) * size.width;
      final y = (ySeed - ySeed.floor()) * size.height;
      final twinkle = 0.35 + 0.65 * math.sin(progress * math.pi * 2 + index);
      stars.color = Colors.white.withValues(alpha: 0.10 + twinkle * 0.28);
      canvas.drawCircle(Offset(x, y), 0.7 + (index % 3) * 0.35, stars);
    }
  }

  void _paintOrbits(Canvas canvas, Size size) {
    final center = _earthCenter(size);
    final radius = _earthRadius(size);
    final orbitPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.1
      ..color = const Color(0xFF5BE7C4).withValues(alpha: 0.15);

    for (final scale in const [1.28, 1.48, 1.72]) {
      canvas.save();
      canvas.translate(center.dx, center.dy);
      canvas.rotate(-0.46 + progress * math.pi * 0.18);
      canvas.scale(1, 0.34);
      canvas.drawCircle(Offset.zero, radius * scale, orbitPaint);
      canvas.restore();
    }

    final satelliteAngle = progress * math.pi * 2;
    final satellite = Offset(
      center.dx + math.cos(satelliteAngle) * radius * 1.72,
      center.dy + math.sin(satelliteAngle) * radius * 0.58,
    );
    canvas.drawCircle(
      satellite,
      3.6,
      Paint()..color = const Color(0xFFFFD38A).withValues(alpha: 0.92),
    );
  }

  void _paintEarth(Canvas canvas, Size size) {
    final center = _earthCenter(size);
    final radius = _earthRadius(size);
    final sphere = Rect.fromCircle(center: center, radius: radius);

    canvas.drawCircle(
      center.translate(radius * 0.10, radius * 0.12),
      radius * 1.06,
      Paint()
        ..shader = RadialGradient(
          colors: [Colors.black.withValues(alpha: 0.46), Colors.transparent],
        ).createShader(sphere.inflate(radius * 0.18)),
    );

    canvas.drawCircle(
      center,
      radius * 1.02,
      Paint()
        ..shader = RadialGradient(
          center: const Alignment(-0.48, -0.62),
          colors: [
            const Color(0xFF8AF8FF).withValues(alpha: 0.32),
            const Color(0xFF124968).withValues(alpha: 0.72),
            Colors.transparent,
          ],
          stops: const [0, 0.64, 1],
        ).createShader(sphere.inflate(radius * 0.18)),
    );

    canvas.save();
    canvas.clipPath(Path()..addOval(sphere));
    canvas.drawOval(
      sphere,
      Paint()
        ..shader = RadialGradient(
          center: const Alignment(-0.42, -0.50),
          radius: 1.08,
          colors: const [
            Color(0xFF7AF6FF),
            Color(0xFF1589B0),
            Color(0xFF073456),
            Color(0xFF030A1A),
          ],
          stops: [0, 0.34, 0.72, 1],
        ).createShader(sphere),
    );

    _paintGrid(canvas, center, radius);
    _paintLand(canvas, center, radius);
    _paintRoutes(canvas, center, radius);
    _paintTerminator(canvas, center, radius, sphere);
    canvas.restore();

    canvas.drawOval(
      sphere,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1.5
        ..color = const Color(0xFFB7FFF5).withValues(alpha: 0.36),
    );
  }

  void _paintGrid(Canvas canvas, Offset center, double radius) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 0.8
      ..color = Colors.white.withValues(alpha: 0.11);

    for (final lat in const [-60, -30, 0, 30, 60]) {
      final y = center.dy - math.sin(_radians(lat.toDouble())) * radius * 0.72;
      final width = math.cos(_radians(lat.toDouble())) * radius;
      canvas.drawOval(
        Rect.fromCenter(
          center: Offset(center.dx, y),
          width: width * 2,
          height: width * 0.38,
        ),
        paint,
      );
    }

    for (var meridian = 0; meridian < 6; meridian++) {
      canvas.save();
      canvas.translate(center.dx, center.dy);
      canvas.rotate((meridian / 6) * math.pi + progress * math.pi * 2);
      canvas.scale(0.20, 1);
      canvas.drawCircle(Offset.zero, radius, paint);
      canvas.restore();
    }
  }

  void _paintLand(Canvas canvas, Offset center, double radius) {
    final landPaint = Paint()
      ..color = const Color(0xFF4EDC9B).withValues(alpha: 0.86);
    final highlightPaint = Paint()
      ..color = const Color(0xFFFFD38A).withValues(alpha: 0.34);

    for (final land in _landMasses) {
      final projected = _project(land.lon, land.lat, center, radius);
      if (projected == null) {
        continue;
      }
      final (:point, :depth) = projected;
      final alpha = (0.30 + depth * 0.70).clamp(0.0, 1.0);
      landPaint.color = const Color(0xFF4EDC9B).withValues(alpha: alpha * 0.88);
      highlightPaint.color = const Color(
        0xFFFFD38A,
      ).withValues(alpha: alpha * 0.26);
      final landRect = Rect.fromCenter(
        center: point,
        width: radius * land.sx * (0.62 + depth * 0.58),
        height: radius * land.sy * (0.56 + depth * 0.36),
      );
      canvas.save();
      canvas.translate(point.dx, point.dy);
      canvas.rotate(math.sin(land.lon) * 0.65);
      canvas.translate(-point.dx, -point.dy);
      canvas.drawOval(landRect, landPaint);
      canvas.drawOval(landRect.deflate(radius * 0.018), highlightPaint);
      canvas.restore();
    }
  }

  void _paintRoutes(Canvas canvas, Offset center, double radius) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 1.4
      ..color = tokens.voiceAccentSoft.withValues(alpha: 0.38);
    final pulsePaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 2.8
      ..color = tokens.voiceAccent.withValues(alpha: 0.44);

    for (var index = 0; index < _routes.length; index++) {
      final route = _routes[index];
      final from = _project(route.fromLon, route.fromLat, center, radius);
      final to = _project(route.toLon, route.toLat, center, radius);
      if (from == null || to == null) {
        continue;
      }
      final path = Path()
        ..moveTo(from.point.dx, from.point.dy)
        ..quadraticBezierTo(
          (from.point.dx + to.point.dx) / 2,
          math.min(from.point.dy, to.point.dy) - radius * (0.18 + index * 0.03),
          to.point.dx,
          to.point.dy,
        );
      canvas.drawPath(path, paint);
      final metric = path.computeMetrics().first;
      final head = ((progress + index * 0.19) % 1) * metric.length;
      final segment = metric.extractPath(
        math.max(0, head - metric.length * 0.12),
        head,
      );
      canvas.drawPath(segment, pulsePaint);
    }
  }

  void _paintTerminator(
    Canvas canvas,
    Offset center,
    double radius,
    Rect sphere,
  ) {
    canvas.drawOval(
      sphere.shift(Offset(radius * 0.22, radius * 0.06)),
      Paint()
        ..shader = LinearGradient(
          begin: Alignment.centerLeft,
          end: Alignment.centerRight,
          colors: [
            Colors.transparent,
            Colors.black.withValues(alpha: 0.18),
            Colors.black.withValues(alpha: 0.68),
          ],
          stops: const [0, 0.52, 1],
        ).createShader(sphere),
    );
    canvas.drawCircle(
      center.translate(-radius * 0.28, -radius * 0.30),
      radius * 0.22,
      Paint()
        ..shader =
            RadialGradient(
              colors: [
                Colors.white.withValues(alpha: 0.22),
                Colors.transparent,
              ],
            ).createShader(
              Rect.fromCircle(
                center: center.translate(-radius * 0.28, -radius * 0.30),
                radius: radius * 0.22,
              ),
            ),
    );
  }

  ({Offset point, double depth})? _project(
    double lon,
    double lat,
    Offset center,
    double radius,
  ) {
    final longitude = _radians(lon + progress * 360);
    final latitude = _radians(lat);
    final x = math.cos(latitude) * math.sin(longitude);
    final z = math.cos(latitude) * math.cos(longitude);
    if (z < -0.18) {
      return null;
    }
    final y = -math.sin(latitude) * 0.72;
    final depthScale = 0.72 + z * 0.28;
    return (
      point: Offset(
        center.dx + x * radius * depthScale,
        center.dy + y * radius * depthScale,
      ),
      depth: ((z + 0.18) / 1.18).clamp(0.0, 1.0),
    );
  }

  Offset _earthCenter(Size size) {
    return Offset(size.width * 0.50, size.height * 0.40);
  }

  double _earthRadius(Size size) {
    final base = math.min(size.width, size.height);
    final radius = base * 0.31;
    return radius.clamp(116.0, 310.0);
  }

  double _radians(double degrees) => degrees * math.pi / 180;

  @override
  bool shouldRepaint(covariant _EarthBackdropPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.tokens != tokens;
  }
}

class _MapLoading extends StatelessWidget {
  const _MapLoading();

  static const _iconPath = 'icons/mapflow-signal-spoon.svg';

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    return Scaffold(
      backgroundColor: tokens.darkSurface,
      body: Center(
        child: SizedBox.square(
          dimension: 112,
          child: Stack(
            alignment: Alignment.center,
            children: [
              SizedBox.square(
                dimension: 96,
                child: CircularProgressIndicator(
                  strokeWidth: 3,
                  color: tokens.voiceAccent,
                  backgroundColor: tokens.onDark.withValues(alpha: 0.1),
                ),
              ),
              ClipOval(
                child: SvgPicture.network(
                  _iconPath,
                  width: 72,
                  height: 72,
                  fit: BoxFit.cover,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _MapTargetLoadingOverlay extends StatelessWidget {
  const _MapTargetLoadingOverlay();

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    return IgnorePointer(
      child: SafeArea(
        child: Align(
          alignment: Alignment.topCenter,
          child: Padding(
            padding: const EdgeInsets.only(top: 18),
            child: DecoratedBox(
              decoration: BoxDecoration(
                color: tokens.mapPanel.withValues(alpha: 0.84),
                borderRadius: BorderRadius.circular(999),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withValues(alpha: 0.24),
                    blurRadius: 22,
                    offset: const Offset(0, 10),
                  ),
                ],
              ),
              child: Padding(
                padding: const EdgeInsets.all(10),
                child: SizedBox.square(
                  dimension: 28,
                  child: CircularProgressIndicator(
                    strokeWidth: 2.5,
                    color: tokens.voiceAccent,
                    backgroundColor: tokens.onDark.withValues(alpha: 0.12),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _MapError extends StatelessWidget {
  const _MapError({required this.message});

  final String message;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return Scaffold(
      body: Stack(
        children: [
          const _MapboxMapLayer(
            focusCenter: LatLng(16.0544, 108.2022),
            places: [],
            selectedPlaceId: null,
            userCoordinate: null,
            onPlaceTap: _ignorePlaceTap,
          ),
          SafeArea(
            child: Align(
              alignment: Alignment.bottomCenter,
              child: Container(
                margin: const EdgeInsets.all(12),
                padding: const EdgeInsets.all(12),
                color: tokens.mapPanel,
                child: Text(
                  message,
                  maxLines: 3,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

void _ignorePlaceTap(PlaceRecommendation _) {}

mbx.Point _mapboxPoint(LatLng coordinate) {
  return mbx.Point(
    coordinates: mbx.Position(coordinate.longitude, coordinate.latitude),
  );
}

String get _mapboxStyleUri {
  if (_mapboxStyle.startsWith('mapbox://')) {
    return _mapboxStyle;
  }
  if (_mapboxStyle.startsWith('mapbox/')) {
    return 'mapbox://styles/$_mapboxStyle';
  }
  return _mapboxStyle;
}

class _TraitPickerSheet extends StatelessWidget {
  const _TraitPickerSheet({
    required this.selectedTrait,
    required this.traits,
    required this.traitCounts,
    required this.onSelect,
  });

  final PlaceTrait? selectedTrait;
  final List<PlaceTrait> traits;
  final Map<PlaceTrait, int> traitCounts;
  final ValueChanged<PlaceTrait> onSelect;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.fromLTRB(16, 18, 16, 18),
        child: Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            for (final item in traits)
              ChoiceChip(
                avatar: Icon(item.icon, size: 17),
                label: Text('${item.label} ${traitCounts[item] ?? 0}'),
                selected: item == selectedTrait,
                onSelected: (_) => onSelect(item),
                backgroundColor: tokens.darkSurfaceAlt,
                selectedColor: Theme.of(context).colorScheme.primaryContainer,
                side: BorderSide(color: tokens.mapPanelBorder),
                shape: const StadiumBorder(),
                padding: const EdgeInsets.symmetric(
                  horizontal: 10,
                  vertical: 9,
                ),
              ),
          ],
        ),
      ),
    );
  }
}

class _PlaceMarker extends StatelessWidget {
  const _PlaceMarker({required this.selected, required this.onTap});

  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final color = Theme.of(context).colorScheme.primary;
    final tokens = context.mapflowTokens;
    return GestureDetector(
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 160),
        decoration: BoxDecoration(
          color: selected ? color : tokens.mapPanel,
          shape: BoxShape.circle,
          border: Border.all(color: color, width: selected ? 3 : 2),
          boxShadow: [
            BoxShadow(
              color: tokens.mapShadow,
              blurRadius: 14,
              offset: const Offset(0, 8),
            ),
          ],
        ),
        child: Icon(Icons.place, color: selected ? tokens.onDark : color),
      ),
    );
  }
}

class _PlaceCarousel extends StatelessWidget {
  const _PlaceCarousel({
    required this.places,
    required this.onSelect,
    required this.onFavoriteToggle,
  });

  final List<PlaceRecommendation> places;
  final ValueChanged<PlaceRecommendation> onSelect;
  final ValueChanged<PlaceRecommendation> onFavoriteToggle;

  @override
  Widget build(BuildContext context) {
    if (places.isEmpty) {
      return const SizedBox.shrink();
    }

    return SizedBox(
      height: 172,
      child: ListView.separated(
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
        itemCount: places.length,
        separatorBuilder: (_, _) => const SizedBox(width: 10),
        itemBuilder: (context, index) {
          final place = places[index];
          return PlacePhotoCard(
            place: place,
            onTap: () => onSelect(place),
            onFavoriteToggle: () => onFavoriteToggle(place),
          );
        },
      ),
    );
  }
}

class _PlaceDetailsSheet extends StatefulWidget {
  const _PlaceDetailsSheet({
    required this.place,
    required this.details,
    required this.scrollController,
    required this.onFavoriteToggle,
    required this.onPhotoTap,
  });

  final PlaceRecommendation place;
  final Future<PlaceRecommendation> details;
  final ScrollController scrollController;
  final ValueChanged<PlaceRecommendation> onFavoriteToggle;
  final void Function(List<String> photoUrls, int index) onPhotoTap;

  @override
  State<_PlaceDetailsSheet> createState() => _PlaceDetailsSheetState();
}

class _PlaceDetailsSheetState extends State<_PlaceDetailsSheet> {
  late var _isFavorite = widget.place.isFavorite;
  var _hoursExpanded = false;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final colorScheme = Theme.of(context).colorScheme;
    return FutureBuilder<PlaceRecommendation>(
      future: widget.details,
      builder: (context, snapshot) {
        final place = snapshot.data ?? widget.place;
        final loadingDetails =
            snapshot.connectionState != ConnectionState.done &&
            place.photoUrls.isEmpty;
        final openingSummary = _openingSummary(place);
        final weekdayDescriptions = _weekdayDescriptions(place);
        final typeLabel = _placeTypeLabel(place);

        return SafeArea(
          top: false,
          child: ListView(
            controller: widget.scrollController,
            padding: const EdgeInsets.fromLTRB(16, 0, 16, 18),
            children: [
              _PlaceDetailsPhotos(
                photoUrls: place.photoUrls,
                loading: loadingDetails,
                onPhotoTap: (index) =>
                    widget.onPhotoTap(place.photoUrls, index),
              ),
              const SizedBox(height: 14),
              Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          place.name,
                          style: Theme.of(context).textTheme.headlineSmall
                              ?.copyWith(
                                fontWeight: FontWeight.w900,
                                letterSpacing: 0,
                              ),
                        ),
                        if (typeLabel != null) ...[
                          const SizedBox(height: 4),
                          Text(
                            typeLabel,
                            style: Theme.of(context).textTheme.bodyMedium
                                ?.copyWith(color: colorScheme.onSurfaceVariant),
                          ),
                        ],
                      ],
                    ),
                  ),
                  IconButton.filledTonal(
                    onPressed: _toggleFavorite,
                    icon: Icon(
                      _isFavorite ? Icons.favorite : Icons.favorite_border,
                    ),
                    tooltip: 'Избранное',
                  ),
                ],
              ),
              const SizedBox(height: 12),
              if (snapshot.hasError) ...[
                Text(
                  'Не удалось загрузить детали заведения',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: colorScheme.error,
                    fontWeight: FontWeight.w700,
                  ),
                ),
                const SizedBox(height: 12),
              ],
              if (openingSummary != null || weekdayDescriptions.isNotEmpty)
                _OpeningHoursTile(
                  summary: openingSummary,
                  weekdayDescriptions: weekdayDescriptions,
                  expanded: _hoursExpanded,
                  onExpansionChanged: (value) =>
                      setState(() => _hoursExpanded = value),
                ),
              if (place.traits.isNotEmpty) ...[
                const SizedBox(height: 16),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    for (final trait in place.traits)
                      Chip(
                        avatar: Icon(trait.icon, size: 16),
                        label: Text(trait.label),
                        side: BorderSide(color: tokens.mapPanelBorder),
                        backgroundColor: tokens.mapPanel,
                      ),
                  ],
                ),
              ],
            ],
          ),
        );
      },
    );
  }

  void _toggleFavorite() {
    setState(() => _isFavorite = !_isFavorite);
    widget.onFavoriteToggle(widget.place);
  }
}

class _OpeningHoursTile extends StatelessWidget {
  const _OpeningHoursTile({
    required this.summary,
    required this.weekdayDescriptions,
    required this.expanded,
    required this.onExpansionChanged,
  });

  final _OpeningSummary? summary;
  final List<String> weekdayDescriptions;
  final bool expanded;
  final ValueChanged<bool> onExpansionChanged;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final colorScheme = Theme.of(context).colorScheme;
    final currentSummary = summary;
    final title = currentSummary?.label ?? 'Режим работы';
    final foreground = currentSummary?.isOpen == true
        ? colorScheme.primary
        : colorScheme.onSurfaceVariant;

    return DecoratedBox(
      decoration: BoxDecoration(
        color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.55),
        borderRadius: BorderRadius.circular(tokens.panelRadius),
        border: Border.all(color: tokens.mapPanelBorder),
      ),
      child: ExpansionTile(
        initiallyExpanded: expanded,
        onExpansionChanged: onExpansionChanged,
        tilePadding: const EdgeInsets.symmetric(horizontal: 12),
        childrenPadding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
        leading: Icon(Icons.schedule_outlined, color: foreground),
        title: Text(
          title,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(color: foreground, fontWeight: FontWeight.w900),
        ),
        trailing: weekdayDescriptions.isEmpty
            ? null
            : Icon(
                expanded
                    ? Icons.expand_less_rounded
                    : Icons.expand_more_rounded,
              ),
        children: [
          if (weekdayDescriptions.isNotEmpty)
            for (final line in weekdayDescriptions)
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 3),
                child: Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    line,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ),
              ),
        ],
      ),
    );
  }
}

class _PlaceDetailsPhotos extends StatelessWidget {
  const _PlaceDetailsPhotos({
    required this.photoUrls,
    required this.loading,
    required this.onPhotoTap,
  });

  final List<String> photoUrls;
  final bool loading;
  final ValueChanged<int> onPhotoTap;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final colorScheme = Theme.of(context).colorScheme;
    if (photoUrls.isEmpty) {
      return AspectRatio(
        aspectRatio: 16 / 9,
        child: DecoratedBox(
          decoration: BoxDecoration(
            color: tokens.mapPanelBorder,
            borderRadius: BorderRadius.circular(tokens.panelRadius),
          ),
          child: Center(
            child: loading
                ? SizedBox.square(
                    dimension: 28,
                    child: CircularProgressIndicator(
                      strokeWidth: 2.4,
                      color: colorScheme.primary,
                    ),
                  )
                : Icon(
                    Icons.place_outlined,
                    color: colorScheme.onSurfaceVariant,
                    size: 40,
                  ),
          ),
        ),
      );
    }

    return SizedBox(
      height: 210,
      child: ListView.separated(
        scrollDirection: Axis.horizontal,
        itemCount: photoUrls.length,
        separatorBuilder: (_, _) => const SizedBox(width: 8),
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () => onPhotoTap(index),
            child: AspectRatio(
              aspectRatio: 4 / 3,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(tokens.panelRadius),
                child: Image.network(
                  _photoUrlForSize(photoUrls[index], maxWidth: 720),
                  fit: BoxFit.cover,
                  gaplessPlayback: true,
                  loadingBuilder: (context, child, loadingProgress) {
                    if (loadingProgress == null) {
                      return child;
                    }
                    return ColoredBox(
                      color: tokens.mapPanelBorder,
                      child: Center(
                        child: SizedBox.square(
                          dimension: 24,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: colorScheme.primary,
                          ),
                        ),
                      ),
                    );
                  },
                  errorBuilder: (_, _, _) => ColoredBox(
                    color: tokens.mapPanelBorder,
                    child: Icon(
                      Icons.broken_image_outlined,
                      color: colorScheme.outline,
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class _PlacePhotoViewer extends StatelessWidget {
  const _PlacePhotoViewer({
    required this.photoUrls,
    required this.initialIndex,
  });

  final List<String> photoUrls;
  final int initialIndex;

  @override
  Widget build(BuildContext context) {
    final controller = PageController(initialPage: initialIndex);
    return Dialog.fullscreen(
      backgroundColor: Colors.black,
      child: Stack(
        children: [
          PageView.builder(
            controller: controller,
            itemCount: photoUrls.length,
            itemBuilder: (context, index) {
              return InteractiveViewer(
                minScale: 1,
                maxScale: 4,
                child: Center(
                  child: Image.network(
                    _photoUrlForSize(photoUrls[index], maxWidth: 1600),
                    fit: BoxFit.contain,
                    loadingBuilder: (context, child, loadingProgress) {
                      if (loadingProgress == null) {
                        return child;
                      }
                      return const CircularProgressIndicator();
                    },
                    errorBuilder: (_, _, _) => const Icon(
                      Icons.broken_image_outlined,
                      color: Colors.white,
                      size: 42,
                    ),
                  ),
                ),
              );
            },
          ),
          SafeArea(
            child: Align(
              alignment: Alignment.topRight,
              child: Padding(
                padding: const EdgeInsets.all(8),
                child: IconButton.filled(
                  onPressed: () => Navigator.of(context).pop(),
                  icon: const Icon(Icons.close),
                  tooltip: 'Закрыть',
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _OpeningSummary {
  const _OpeningSummary({required this.label, required this.isOpen});

  final String label;
  final bool isOpen;
}

class _OpeningPoint {
  const _OpeningPoint({
    required this.googleDay,
    required this.hour,
    required this.minute,
  });

  final int googleDay;
  final int hour;
  final int minute;
}

_OpeningSummary? _openingSummary(PlaceRecommendation place) {
  final openNow = _openNow(place);
  final periods = _periods(place);
  final now = DateTime.now();

  if (openNow == true) {
    final closeAt = _nextOpeningBoundary(
      periods,
      now,
      boundary: _OpeningBoundary.close,
    );
    if (closeAt != null) {
      return _OpeningSummary(
        label: 'Открыто до ${_formatTime(closeAt)}',
        isOpen: true,
      );
    }
    return const _OpeningSummary(label: 'Открыто', isOpen: true);
  }

  if (openNow == false) {
    final openAt = _nextOpeningBoundary(
      periods,
      now,
      boundary: _OpeningBoundary.open,
    );
    if (openAt != null) {
      return _OpeningSummary(
        label: 'Откроется ${_formatRelativeTime(openAt, now)}',
        isOpen: false,
      );
    }
    return const _OpeningSummary(label: 'Закрыто', isOpen: false);
  }

  return null;
}

bool? _openNow(PlaceRecommendation place) {
  final value = place.googleCurrentOpeningHours?['openNow'];
  if (value is bool) {
    return value;
  }
  final legacyValue = place.googleCurrentOpeningHours?['open_now'];
  if (legacyValue is bool) {
    return legacyValue;
  }
  return null;
}

enum _OpeningBoundary { open, close }

DateTime? _nextOpeningBoundary(
  List<Map<String, dynamic>> periods,
  DateTime now, {
  required _OpeningBoundary boundary,
}) {
  DateTime? next;
  for (final period in periods) {
    final point = _openingPoint(period[boundary.name]);
    if (point == null) {
      continue;
    }
    final candidate = _nextDateForOpeningPoint(point, now);
    if (candidate.isBefore(now)) {
      continue;
    }
    if (next == null || candidate.isBefore(next)) {
      next = candidate;
    }
  }
  return next;
}

DateTime _nextDateForOpeningPoint(_OpeningPoint point, DateTime now) {
  final todayGoogleDay = now.weekday % 7;
  var daysUntil = (point.googleDay - todayGoogleDay) % 7;
  var candidate = DateTime(
    now.year,
    now.month,
    now.day,
    point.hour,
    point.minute,
  ).add(Duration(days: daysUntil));

  if (!candidate.isAfter(now)) {
    candidate = candidate.add(const Duration(days: 7));
  }
  return candidate;
}

List<Map<String, dynamic>> _periods(PlaceRecommendation place) {
  final current = place.googleCurrentOpeningHours?['periods'];
  final regular = place.googleRegularOpeningHours?['periods'];
  final source = current is List ? current : regular;
  if (source is! List) {
    return const [];
  }
  return [
    for (final item in source)
      if (item is Map) item.map((key, value) => MapEntry('$key', value)),
  ];
}

_OpeningPoint? _openingPoint(Object? value) {
  if (value is! Map) {
    return null;
  }

  final day = _intValue(value['day']);
  final time = value['time'];
  final hour = _intValue(value['hour']);
  final minute = _intValue(value['minute']) ?? 0;

  if (day == null) {
    return null;
  }
  if (hour != null) {
    return _OpeningPoint(
      googleDay: day,
      hour: hour.clamp(0, 23),
      minute: minute.clamp(0, 59),
    );
  }
  if (time is String && time.length >= 4) {
    final parsedHour = int.tryParse(time.substring(0, 2));
    final parsedMinute = int.tryParse(time.substring(2, 4));
    if (parsedHour != null && parsedMinute != null) {
      return _OpeningPoint(
        googleDay: day,
        hour: parsedHour.clamp(0, 23),
        minute: parsedMinute.clamp(0, 59),
      );
    }
  }

  return null;
}

int? _intValue(Object? value) {
  if (value is int) {
    return value;
  }
  if (value is num) {
    return value.toInt();
  }
  if (value is String) {
    return int.tryParse(value);
  }
  return null;
}

String _formatRelativeTime(DateTime date, DateTime now) {
  final difference = date.difference(now);
  if (difference.inMinutes > 0 && difference.inMinutes < 60) {
    return 'через ${difference.inMinutes} мин';
  }
  if (difference.inHours > 0 && difference.inHours < 6) {
    return 'через ${difference.inHours} ч';
  }
  final isToday = _sameDate(date, now);
  if (isToday) {
    return 'в ${_formatTime(date)}';
  }
  final tomorrow = now.add(const Duration(days: 1));
  if (_sameDate(date, tomorrow)) {
    return 'завтра в ${_formatTime(date)}';
  }
  return '${_weekdayShort(date)} в ${_formatTime(date)}';
}

bool _sameDate(DateTime a, DateTime b) {
  return a.year == b.year && a.month == b.month && a.day == b.day;
}

String _formatTime(DateTime date) {
  final hour = date.hour.toString().padLeft(2, '0');
  final minute = date.minute.toString().padLeft(2, '0');
  return '$hour:$minute';
}

String _weekdayShort(DateTime date) {
  return switch (date.weekday) {
    DateTime.monday => 'пн',
    DateTime.tuesday => 'вт',
    DateTime.wednesday => 'ср',
    DateTime.thursday => 'чт',
    DateTime.friday => 'пт',
    DateTime.saturday => 'сб',
    _ => 'вс',
  };
}

List<String> _weekdayDescriptions(PlaceRecommendation place) {
  final current = _stringList(
    place.googleCurrentOpeningHours?['weekdayDescriptions'],
  );
  if (current.isNotEmpty) {
    return current;
  }
  final regular = _stringList(
    place.googleRegularOpeningHours?['weekdayDescriptions'],
  );
  if (regular.isNotEmpty) {
    return regular;
  }
  final legacy = _stringList(place.googleRegularOpeningHours?['weekday_text']);
  if (legacy.isNotEmpty) {
    return legacy;
  }
  return const [];
}

List<String> _stringList(Object? value) {
  if (value is! List) {
    return const [];
  }
  return [
    for (final item in value)
      if (item is String && item.trim().isNotEmpty) item,
  ];
}

String _photoUrlForSize(String url, {required int maxWidth}) {
  final uri = Uri.tryParse(url);
  if (uri == null || !uri.hasScheme) {
    return url;
  }

  final params = Map<String, String>.from(uri.queryParameters);
  if (params.containsKey('maxWidthPx') || params.containsKey('maxHeightPx')) {
    params
      ..update('maxWidthPx', (_) => '$maxWidth', ifAbsent: () => '$maxWidth')
      ..remove('maxHeightPx');
    return uri.replace(queryParameters: params).toString();
  }

  if (params.containsKey('maxwidth') || params.containsKey('maxheight')) {
    params
      ..update('maxwidth', (_) => '$maxWidth', ifAbsent: () => '$maxWidth')
      ..remove('maxheight');
    return uri.replace(queryParameters: params).toString();
  }

  if (uri.host.contains('places.googleapis.com')) {
    params['maxWidthPx'] = '$maxWidth';
    return uri.replace(queryParameters: params).toString();
  }

  if (uri.host.contains('googleapis.com')) {
    params['maxwidth'] = '$maxWidth';
    return uri.replace(queryParameters: params).toString();
  }

  return url;
}

String? _placeTypeLabel(PlaceRecommendation place) {
  final raw =
      place.googlePrimaryType ??
      (place.googleTypes.isEmpty ? null : place.googleTypes.first);
  if (raw == null || raw.trim().isEmpty) {
    return null;
  }
  return raw.replaceAll('_', ' ');
}

class AddExperienceFlow extends StatefulWidget {
  const AddExperienceFlow({super.key, required this.coordinate});

  final LatLng? coordinate;

  @override
  State<AddExperienceFlow> createState() => _AddExperienceFlowState();
}

class _AddExperienceFlowState extends State<AddExperienceFlow> {
  static const _minimumInformationUnits = 16.0;
  static const _nearbyPlaceRadiusMeters = 50;
  static const _voicePromptHints = [
    'атмосфера',
    'еда',
    'сервис',
    'люди',
    'шум',
    'цены',
  ];

  final _waveController = WaveformRecorderController(
    interval: const Duration(milliseconds: 45),
    config: const RecordConfig(
      numChannels: 1,
      sampleRate: 44100,
      autoGain: true,
      echoCancel: true,
      noiseSuppress: true,
    ),
  );

  Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
  StreamSubscription<Amplitude>? _amplitudeSub;
  var _step = 0;
  var _informationUnits = 0.0;
  var _recording = false;
  var _submitting = false;
  var _micAllowed = true;
  var _noiseDb = -72.0;
  var _voicePeakDb = -34.0;
  var _liveLevel = 0.0;
  DateTime? _lastInformationAt;
  PlaceRecommendation? _selectedPlaceForSubmit;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _amplitudeSub?.cancel();
    _waveController.dispose();
    super.dispose();
  }

  Future<void> _toggleRecording() async {
    if (_recording) {
      await _stopRecording();
      return;
    }

    await _startRecording();
  }

  Future<List<PlaceRecommendation>> _loadNearbyPlaces() async {
    final coordinate = widget.coordinate;
    if (coordinate == null) {
      return const [];
    }

    return context.read<PlacesRepository>().fetchNearbyPlaces(
      coordinate: coordinate,
      radiusMeters: _nearbyPlaceRadiusMeters,
    );
  }

  Future<void> _startRecording() async {
    await _waveController.startRecording();
    await _amplitudeSub?.cancel();
    _lastInformationAt = DateTime.now();
    _amplitudeSub = _waveController.amplitudeStream.listen(_handleAmplitude);

    setState(() {
      _micAllowed = true;
      _recording = true;
      _liveLevel = 0;
      _informationUnits = 0;
    });
  }

  Future<void> _stopRecording() async {
    await _amplitudeSub?.cancel();
    _amplitudeSub = null;
    await _waveController.stopRecording();
    _lastInformationAt = null;
    if (!mounted) {
      return;
    }
    setState(() {
      _recording = false;
      _liveLevel = 0;
    });
  }

  void _handleAmplitude(Amplitude amplitude) {
    final currentDb = amplitude.current;
    final now = DateTime.now();
    final level = _normalizeDbLevel(currentDb);
    final informationDelta = _consumeInformationDelta(level, now);

    setState(() {
      _liveLevel = _smoothLevel(_liveLevel, level);
      _informationUnits = math.min(
        _minimumInformationUnits,
        _informationUnits + informationDelta,
      );
    });
    context.read<PlaceCubit>().setReviewDuration(_waveController.timeElapsed);
  }

  double _normalizeDbLevel(double currentDb) {
    final db = currentDb.clamp(-160.0, 0.0);
    if (db < _noiseDb) {
      _noiseDb = _noiseDb * 0.90 + db * 0.10;
    } else {
      _noiseDb = _noiseDb * 0.995 + db * 0.005;
    }
    if (db > _voicePeakDb) {
      _voicePeakDb = _voicePeakDb * 0.72 + db * 0.28;
    } else {
      _voicePeakDb = math.max(_noiseDb + 18, _voicePeakDb * 0.998 + db * 0.002);
    }

    final range = math.max(18.0, _voicePeakDb - _noiseDb);
    final gated = ((db - _noiseDb - 5) / range).clamp(0.0, 1.0);
    return math.pow(gated, 0.62).toDouble();
  }

  double _smoothLevel(double current, double next) {
    final weight = next > current ? 0.46 : 0.18;
    return current + (next - current) * weight;
  }

  double _consumeInformationDelta(double voicedAmount, DateTime now) {
    final previous = _lastInformationAt ?? now;
    _lastInformationAt = now;
    final deltaSeconds =
        now.difference(previous).inMilliseconds.clamp(20, 180) / 1000;
    return voicedAmount.clamp(0.0, 1.0) * deltaSeconds;
  }

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final controller = context.read<PlaceCubit>();
    final hasTelegramAuth = context.select(
      (PlaceCubit cubit) => cubit.state.placeState?.hasTelegramAuth ?? false,
    );
    final informationProgress = (_informationUnits / _minimumInformationUnits)
        .clamp(0.0, 1.0);
    final content = switch (_step) {
      0 => _VoiceStep(
        placeName: '',
        promptHints: _voicePromptHints,
        hasTelegramAuth: hasTelegramAuth,
        informationProgress: informationProgress,
        isRecording: _recording,
        isSubmitting: _submitting,
        micAllowed: _micAllowed,
        liveLevel: _liveLevel,
        canContinue: hasTelegramAuth && informationProgress >= 1,
        onToggleRecording: _toggleRecording,
        onNext: () async {
          if (_recording) {
            await _stopRecording();
          }
          setState(() {
            _nearbyPlacesFuture = _loadNearbyPlaces();
            _step = 1;
          });
        },
      ),
      1 => _PlaceStep(
        placesFuture: _nearbyPlacesFuture,
        radiusMeters: _nearbyPlaceRadiusMeters,
        isSubmitting: _submitting,
        onSelect: (place) async {
          controller.setReviewPlace(place.name);
          setState(() {
            _selectedPlaceForSubmit = place;
            _step = 2;
          });
        },
      ),
      _ => const SizedBox.shrink(),
    };
    final contentWithFavoriteStep = _step == 2
        ? _FavoriteStep(
            place: _selectedPlaceForSubmit,
            isSubmitting: _submitting,
            onSubmit: _submitSelectedPlace,
          )
        : content;

    return Scaffold(
      backgroundColor: tokens.darkSurface,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.fromLTRB(
            16,
            10,
            16,
            MediaQuery.viewInsetsOf(context).bottom + 18,
          ),
          child: Column(
            children: [
              _StoryProgress(
                step: _step,
                total: 3,
                dark: true,
                onClose: () => context.pop(),
              ),
              const SizedBox(height: 18),
              Expanded(
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 220),
                  child: KeyedSubtree(
                    key: ValueKey(_step),
                    child: contentWithFavoriteStep,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _submitSelectedPlace(bool addToFavorites) async {
    final place = _selectedPlaceForSubmit;
    if (place == null) {
      throw StateError('Place is required to publish a review.');
    }
    setState(() => _submitting = true);
    final controller = context.read<PlaceCubit>();
    final file = _waveController.file;
    if (file == null) {
      throw StateError('Voice recording file is required.');
    }
    final bytes = await file.readAsBytes();
    await controller.publishReview(
      place: place,
      audioObjectKey:
          'web-recording-${DateTime.now().microsecondsSinceEpoch}-${file.name}',
      audioContentBase64: base64Encode(bytes),
      audioMimeType: file.mimeType ?? 'audio/wav',
      addToFavorites: addToFavorites,
      promptHintsShown: _voicePromptHints,
      recordingPayload: {
        'source': 'web',
        'nearbyPlaceRadiusMeters': _nearbyPlaceRadiusMeters,
        'informationUnits': _informationUnits,
        'minimumInformationUnits': _minimumInformationUnits,
        'selectedPlace': {
          'googlePlaceId': place.googlePlaceId,
          'name': place.name,
          'googlePrimaryType': place.googlePrimaryType,
          'googleTypes': place.googleTypes,
          'latitude': place.coordinate.latitude,
          'longitude': place.coordinate.longitude,
        },
      },
    );
    if (!mounted) {
      return;
    }
    context.pop();
  }
}

class _PlaceStep extends StatelessWidget {
  const _PlaceStep({
    required this.placesFuture,
    required this.radiusMeters,
    required this.isSubmitting,
    required this.onSelect,
  });

  final Future<List<PlaceRecommendation>>? placesFuture;
  final int radiusMeters;
  final bool isSubmitting;
  final Future<void> Function(PlaceRecommendation) onSelect;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return _StepLayout(
      body: Column(
        children: [
          Text(
            'Выбери место рядом',
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.headlineSmall?.copyWith(
              color: Colors.white,
              fontWeight: FontWeight.w900,
              letterSpacing: 0,
            ),
          ),
          const SizedBox(height: 16),
          Expanded(
            child: FutureBuilder<List<PlaceRecommendation>>(
              future: placesFuture,
              builder: (context, snapshot) {
                if (snapshot.connectionState != ConnectionState.done) {
                  return Center(
                    child: CircularProgressIndicator(color: tokens.voiceAccent),
                  );
                }
                if (snapshot.hasError) {
                  return Center(
                    child: Icon(
                      Icons.error_outline,
                      color: tokens.onDark,
                      size: 42,
                    ),
                  );
                }

                final places = snapshot.data ?? const <PlaceRecommendation>[];
                if (places.isEmpty) {
                  return Center(
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(
                          Icons.location_off_outlined,
                          color: tokens.onDark,
                          size: 42,
                        ),
                        const SizedBox(height: 10),
                        Text(
                          'Нет мест в $radiusMetersм',
                          style: TextStyle(color: tokens.onDark),
                        ),
                      ],
                    ),
                  );
                }

                return ListView.separated(
                  itemCount: places.length,
                  separatorBuilder: (_, _) => const SizedBox(height: 10),
                  itemBuilder: (context, index) {
                    final place = places[index];
                    return _NearbyPlaceCard(
                      place: place,
                      disabled: isSubmitting,
                      onTap: () => onSelect(place),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class _FavoriteStep extends StatelessWidget {
  const _FavoriteStep({
    required this.place,
    required this.isSubmitting,
    required this.onSubmit,
  });

  final PlaceRecommendation? place;
  final bool isSubmitting;
  final Future<void> Function(bool) onSubmit;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final selectedPlace = place;
    if (selectedPlace == null) {
      return const SizedBox.shrink();
    }

    return _StepLayout(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.favorite_border, color: tokens.onDark, size: 48),
          const SizedBox(height: 18),
          Text(
            selectedPlace.name,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.headlineSmall?.copyWith(
              color: tokens.onDark,
              fontWeight: FontWeight.w900,
              letterSpacing: 0,
            ),
          ),
          const SizedBox(height: 26),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: isSubmitting ? null : () => onSubmit(false),
                  child: const Text('Не сейчас'),
                ),
              ),
              const SizedBox(width: 10),
              Expanded(
                child: FilledButton(
                  onPressed: isSubmitting ? null : () => onSubmit(true),
                  child: isSubmitting
                      ? const SizedBox.square(
                          dimension: 18,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Text('В избранное'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _NearbyPlaceCard extends StatelessWidget {
  const _NearbyPlaceCard({
    required this.place,
    required this.disabled,
    required this.onTap,
  });

  final PlaceRecommendation place;
  final bool disabled;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final primaryType = _formatType(place.googlePrimaryType);
    final tokens = context.mapflowTokens;

    return Material(
      color: tokens.darkSurfaceAlt,
      borderRadius: BorderRadius.circular(tokens.panelRadius),
      child: InkWell(
        onTap: disabled ? null : onTap,
        borderRadius: BorderRadius.circular(tokens.panelRadius),
        child: Padding(
          padding: const EdgeInsets.all(14),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Icon(
                    Icons.place_outlined,
                    color: tokens.onDark.withValues(alpha: 0.70),
                  ),
                  const SizedBox(width: 10),
                  Expanded(
                    child: Text(
                      place.name,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: TextStyle(
                        color: tokens.onDark,
                        fontWeight: FontWeight.w900,
                        height: 1.1,
                      ),
                    ),
                  ),
                ],
              ),
              if (primaryType != null) ...[
                const SizedBox(height: 12),
                Wrap(
                  spacing: 6,
                  runSpacing: 6,
                  children: [_PlaceTypeChip(label: primaryType, primary: true)],
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }

  String? _formatType(String? type) {
    if (type == null || type.isEmpty) {
      return null;
    }
    return type.replaceAll('_', ' ');
  }
}

class _PlaceTypeChip extends StatelessWidget {
  const _PlaceTypeChip({required this.label, required this.primary});

  final String label;
  final bool primary;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return DecoratedBox(
      decoration: BoxDecoration(
        color: primary
            ? tokens.voiceAccent
            : tokens.onDark.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(tokens.panelRadius),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
        child: Text(
          label,
          style: TextStyle(
            color: primary
                ? tokens.onDark
                : tokens.onDark.withValues(alpha: 0.92),
            fontSize: 12,
            fontWeight: FontWeight.w700,
            height: 1,
          ),
        ),
      ),
    );
  }
}

class _VoiceStep extends StatelessWidget {
  const _VoiceStep({
    required this.placeName,
    required this.promptHints,
    required this.hasTelegramAuth,
    required this.informationProgress,
    required this.isRecording,
    required this.isSubmitting,
    required this.micAllowed,
    required this.liveLevel,
    required this.canContinue,
    required this.onToggleRecording,
    required this.onNext,
  });

  final String placeName;
  final List<String> promptHints;
  final bool hasTelegramAuth;
  final double informationProgress;
  final bool isRecording;
  final bool isSubmitting;
  final bool micAllowed;
  final double liveLevel;
  final bool canContinue;
  final Future<void> Function() onToggleRecording;
  final VoidCallback onNext;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final canFinish = canContinue;

    return Stack(
      children: [
        Column(
          children: [
            if (placeName.trim().isNotEmpty) ...[
              Text(
                placeName.trim(),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                textAlign: TextAlign.center,
                style: Theme.of(
                  context,
                ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
              ),
              const SizedBox(height: 10),
            ],
            Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
            if (!micAllowed)
              Padding(
                padding: const EdgeInsets.only(bottom: 10),
                child: Icon(
                  Icons.mic_off_outlined,
                  color: tokens.voiceAccentSoft,
                  size: 22,
                ),
              ),
            _VoiceRecordButton(
              progress: informationProgress,
              liveLevel: liveLevel,
              isRecording: isRecording,
              canFinish: canFinish,
              enabled: hasTelegramAuth && !isSubmitting,
              onPressed: canFinish ? onNext : onToggleRecording,
            ),
          ],
        ),
        Positioned.fill(
          bottom: 230,
          child: _FloatingPromptHints(
            hints: promptHints,
            active: isRecording || informationProgress < 1,
          ),
        ),
      ],
    );
  }
}

class _FloatingPromptHints extends StatefulWidget {
  const _FloatingPromptHints({required this.hints, required this.active});

  final List<String> hints;
  final bool active;

  @override
  State<_FloatingPromptHints> createState() => _FloatingPromptHintsState();
}

class _FloatingPromptHintsState extends State<_FloatingPromptHints>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 13500),
    );
    _syncAnimation();
  }

  @override
  void didUpdateWidget(covariant _FloatingPromptHints oldWidget) {
    super.didUpdateWidget(oldWidget);
    _syncAnimation();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _syncAnimation() {
    if (widget.active && !_controller.isAnimating) {
      _controller.repeat();
    }
    if (!widget.active && _controller.isAnimating) {
      _controller.stop();
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.hints.isEmpty) {
      return const SizedBox.shrink();
    }

    return IgnorePointer(
      child: AnimatedOpacity(
        duration: const Duration(milliseconds: 260),
        opacity: widget.active ? 1 : 0,
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, _) {
            return LayoutBuilder(
              builder: (context, constraints) {
                return Stack(
                  fit: StackFit.expand,
                  children: [
                    for (var index = 0; index < widget.hints.length; index++)
                      _FloatingPromptHint(
                        label: widget.hints[index],
                        index: index,
                        progress: _controller.value,
                        size: constraints.biggest,
                      ),
                  ],
                );
              },
            );
          },
        ),
      ),
    );
  }
}

class _FloatingPromptHint extends StatelessWidget {
  const _FloatingPromptHint({
    required this.label,
    required this.index,
    required this.progress,
    required this.size,
  });

  final String label;
  final int index;
  final double progress;
  final Size size;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;
    final phase = (progress + index * 0.173) % 1.0;
    final eased = Curves.easeInOutCubic.transform(phase);
    final visibility = _visibilityForPhase(phase);
    final seed = index + 1.0;
    final start = _alignment(seed, 0);
    final end = _alignment(seed, 1);
    final alignment = Alignment(
      _lerpDouble(start.x, end.x, eased),
      _lerpDouble(start.y, end.y, eased),
    );
    final drift = Offset(
      math.sin((phase * math.pi * 2) + seed) * size.width * 0.035,
      math.cos((phase * math.pi * 2) + seed * 1.9) * size.height * 0.026,
    );
    final scale =
        0.76 +
        visibility * 0.28 +
        math.sin((phase * math.pi * 2) + seed * 2.4) * 0.035;
    final rotation = math.sin((phase * math.pi * 2) + seed) * 0.035;

    return Align(
      alignment: alignment,
      child: Transform.translate(
        offset: drift,
        child: Transform.rotate(
          angle: rotation,
          child: Transform.scale(
            scale: scale,
            child: Opacity(
              opacity: visibility,
              child: DecoratedBox(
                decoration: BoxDecoration(
                  color: tokens.voiceAccent.withValues(alpha: 0.88),
                  borderRadius: BorderRadius.circular(999),
                  border: Border.all(
                    color: tokens.onDark.withValues(alpha: 0.16),
                  ),
                  boxShadow: [
                    BoxShadow(
                      color: tokens.voiceAccent.withValues(alpha: 0.30),
                      blurRadius: 26,
                      spreadRadius: 1,
                    ),
                  ],
                ),
                child: Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 13,
                    vertical: 8,
                  ),
                  child: Text(
                    label,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(
                      color: tokens.onVoiceControl,
                      fontWeight: FontWeight.w900,
                      letterSpacing: 0,
                      height: 1,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  double _visibilityForPhase(double phase) {
    if (phase < 0.16) {
      return Curves.easeOutCubic.transform(phase / 0.16);
    }
    if (phase > 0.70) {
      return Curves.easeInCubic.transform((1 - phase) / 0.30);
    }
    return 1;
  }

  Alignment _alignment(double seed, int salt) {
    final xSeed = math.sin(seed * 19.73 + salt * 41.11) * 10000;
    final ySeed = math.sin(seed * 47.31 + salt * 17.17) * 10000;
    final x = ((xSeed - xSeed.floor()) * 1.76 - 0.88).clamp(-0.88, 0.88);
    final y = ((ySeed - ySeed.floor()) * 1.58 - 0.79).clamp(-0.79, 0.79);
    return Alignment(x, y);
  }

  double _lerpDouble(double a, double b, double t) => a + (b - a) * t;
}

class _VoiceProgressGrid extends StatelessWidget {
  const _VoiceProgressGrid({required this.progress});

  final double progress;

  @override
  Widget build(BuildContext context) {
    const columns = 18;
    const rows = 12;
    const total = columns * rows;
    final filled = (progress.clamp(0.0, 1.0) * total).round();

    return LayoutBuilder(
      builder: (context, constraints) {
        const gap = 4.0;
        final maxCellWidth =
            (constraints.maxWidth - gap * (columns - 1)) / columns;
        final maxCellHeight = (constraints.maxHeight - gap * (rows - 1)) / rows;
        final cellSize = math.max(
          10.0,
          math.min(maxCellWidth, maxCellHeight).clamp(10.0, 28.0),
        );
        final gridWidth = columns * cellSize + gap * (columns - 1);
        final gridHeight = rows * cellSize + gap * (rows - 1);

        return Center(
          child: SizedBox(
            width: gridWidth,
            height: gridHeight,
            child: Wrap(
              spacing: gap,
              runSpacing: gap,
              children: [
                for (var index = 0; index < total; index++)
                  _VoiceProgressCell(
                    filled: _gridOrder(index, columns, rows) < filled,
                    size: cellSize,
                  ),
              ],
            ),
          ),
        );
      },
    );
  }

  static int _gridOrder(int index, int columns, int rows) {
    final row = index ~/ columns;
    final column = index % columns;
    final centerX = (columns - 1) / 2;
    final centerY = (rows - 1) / 2;
    final dx = column - centerX;
    final dy = row - centerY;
    final radius = math.sqrt(dx * dx + dy * dy);
    final angle = math.atan2(dy, dx);
    final shell = (radius * 7.0 + angle * 5.0).floor();
    final noise = (math.sin((column + 1) * 37.17 + (row + 1) * 91.43) * 10000)
        .abs();
    return ((shell * 31 + noise.floor()) % (columns * rows));
  }
}

class _VoiceProgressCell extends StatelessWidget {
  const _VoiceProgressCell({required this.filled, required this.size});

  final bool filled;
  final double size;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return AnimatedContainer(
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOutCubic,
      width: size,
      height: size,
      decoration: BoxDecoration(
        color: filled
            ? tokens.voiceAccent
            : tokens.onDark.withValues(alpha: 0.11),
        borderRadius: BorderRadius.circular(3),
        boxShadow: filled
            ? [
                BoxShadow(
                  color: tokens.voiceAccent.withValues(alpha: 0.34),
                  blurRadius: 12,
                  spreadRadius: 1,
                ),
              ]
            : null,
      ),
    );
  }
}

class _VoiceRecordButton extends StatefulWidget {
  const _VoiceRecordButton({
    required this.progress,
    required this.liveLevel,
    required this.isRecording,
    required this.canFinish,
    required this.enabled,
    required this.onPressed,
  });

  final double progress;
  final double liveLevel;
  final bool isRecording;
  final bool canFinish;
  final bool enabled;
  final VoidCallback onPressed;

  @override
  State<_VoiceRecordButton> createState() => _VoiceRecordButtonState();
}

class _VoiceRecordButtonState extends State<_VoiceRecordButton>
    with SingleTickerProviderStateMixin {
  late final AnimationController _pulseController;

  @override
  void initState() {
    super.initState();
    _pulseController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1400),
    );
    _syncPulse();
  }

  @override
  void didUpdateWidget(covariant _VoiceRecordButton oldWidget) {
    super.didUpdateWidget(oldWidget);
    _syncPulse();
  }

  @override
  void dispose() {
    _pulseController.dispose();
    super.dispose();
  }

  void _syncPulse() {
    if (widget.isRecording && !_pulseController.isAnimating) {
      _pulseController.repeat();
    }
    if (!widget.isRecording && _pulseController.isAnimating) {
      _pulseController.stop();
      _pulseController.value = 0;
    }
  }

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return SizedBox(
      width: 280,
      height: 280,
      child: AnimatedBuilder(
        animation: _pulseController,
        builder: (context, child) {
          final pulse = widget.isRecording ? _pulseController.value : 0.0;
          final level = widget.isRecording ? widget.liveLevel : 0.0;
          return Stack(
            alignment: Alignment.center,
            children: [
              for (final offset in const [0.0, 0.32, 0.64])
                Transform.scale(
                  scale:
                      1 +
                      ((pulse + offset) % 1) * (0.20 + level * 0.44) +
                      level * 0.16,
                  child: Container(
                    width: 190,
                    height: 190,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      border: Border.all(
                        color: tokens.voiceAccent.withValues(
                          alpha: widget.isRecording
                              ? ((0.06 + level * 0.26) *
                                    (1 - ((pulse + offset) % 1)))
                              : 0,
                        ),
                        width: 2.5 + level * 2.5,
                      ),
                    ),
                  ),
                ),
              SizedBox(
                width: 216,
                height: 216,
                child: CircularProgressIndicator(
                  value: widget.progress,
                  strokeWidth: 8,
                  strokeCap: StrokeCap.round,
                  color: tokens.voiceAccent,
                  backgroundColor: tokens.onDark.withValues(alpha: 0.12),
                ),
              ),
              child!,
            ],
          );
        },
        child: SizedBox(
          width: 156,
          height: 156,
          child: FilledButton(
            onPressed: widget.enabled ? widget.onPressed : null,
            style: FilledButton.styleFrom(
              backgroundColor: tokens.onDark,
              foregroundColor: tokens.onVoiceControl,
              disabledBackgroundColor: tokens.onDark.withValues(alpha: 0.28),
              disabledForegroundColor: tokens.onDark.withValues(alpha: 0.52),
              shape: const CircleBorder(),
              padding: EdgeInsets.zero,
              elevation: 0,
            ),
            child: Icon(
              widget.canFinish
                  ? Icons.check_rounded
                  : widget.isRecording
                  ? Icons.pause_rounded
                  : Icons.mic_rounded,
              size: 56,
            ),
          ),
        ),
      ),
    );
  }
}

class _StepLayout extends StatelessWidget {
  const _StepLayout({required this.body});

  final Widget body;

  @override
  Widget build(BuildContext context) {
    return Column(children: [Expanded(child: body)]);
  }
}

class _StoryProgress extends StatelessWidget {
  const _StoryProgress({
    required this.step,
    required this.total,
    required this.dark,
    required this.onClose,
  });

  final int step;
  final int total;
  final bool dark;
  final VoidCallback onClose;

  @override
  Widget build(BuildContext context) {
    final tokens = context.mapflowTokens;

    return Row(
      children: [
        Expanded(
          child: Row(
            children: [
              for (var index = 0; index < total; index++) ...[
                Expanded(
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 180),
                    height: 5,
                    decoration: BoxDecoration(
                      color: index <= step
                          ? (dark
                                ? tokens.voiceAccent
                                : Theme.of(context).colorScheme.primary)
                          : (dark
                                ? tokens.onDark.withValues(alpha: 0.16)
                                : tokens.mapPanelBorder),
                      borderRadius: BorderRadius.circular(99),
                    ),
                  ),
                ),
                if (index != total - 1) const SizedBox(width: 6),
              ],
            ],
          ),
        ),
        const SizedBox(width: 10),
        IconButton(
          onPressed: onClose,
          icon: Icon(Icons.close, color: dark ? Colors.white : null),
          tooltip: 'Закрыть',
        ),
      ],
    );
  }
}
