import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';

import '../../../shared/location/current_location.dart';
import '../data/auth_repository.dart';
import '../data/places_repository.dart';
import '../data/voice_experiences_repository.dart';
import '../domain/place_models.dart';

const _unset = Object();

enum PlaceLoadStatus { loading, ready, failure }

class PlaceState {
  const PlaceState({
    required this.selectedTrait,
    required this.places,
    required this.selectedPlaceId,
    required this.currentUser,
    required this.hasTelegramAuth,
    required this.userCoordinate,
    required this.reviewDraft,
    required this.voiceFilterTranscript,
    required this.voiceFilterTags,
  });

  static final _distance = Distance();

  final PlaceTrait? selectedTrait;
  final List<PlaceRecommendation> places;
  final String? selectedPlaceId;
  final AppUser? currentUser;
  final bool hasTelegramAuth;
  final LatLng? userCoordinate;
  final VoiceReviewDraft reviewDraft;
  final String voiceFilterTranscript;
  final List<String> voiceFilterTags;

  List<PlaceRecommendation> get recommendations {
    final selected = selectedTrait;
    final matchingPlaces = selected == null
        ? places
        : places.where((place) => place.traits.contains(selected));
    final coordinate = userCoordinate;
    if (coordinate == null) {
      return matchingPlaces.toList();
    }

    final ranked = [...matchingPlaces]
      ..sort((a, b) {
        return _distance(
          coordinate,
          a.coordinate,
        ).compareTo(_distance(coordinate, b.coordinate));
      });
    return ranked;
  }

  PlaceRecommendation? get selectedPlace {
    final visiblePlaces = recommendations;
    for (final place in visiblePlaces) {
      if (place.id == selectedPlaceId) {
        return place;
      }
    }
    return null;
  }

  PlaceState copyWith({
    Object? selectedTrait = _unset,
    List<PlaceRecommendation>? places,
    Object? selectedPlaceId = _unset,
    Object? currentUser = _unset,
    bool? hasTelegramAuth,
    Object? userCoordinate = _unset,
    VoiceReviewDraft? reviewDraft,
    String? voiceFilterTranscript,
    List<String>? voiceFilterTags,
  }) {
    return PlaceState(
      selectedTrait: identical(selectedTrait, _unset)
          ? this.selectedTrait
          : selectedTrait as PlaceTrait?,
      places: places ?? this.places,
      selectedPlaceId: identical(selectedPlaceId, _unset)
          ? this.selectedPlaceId
          : selectedPlaceId as String?,
      currentUser: identical(currentUser, _unset)
          ? this.currentUser
          : currentUser as AppUser?,
      hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
      userCoordinate: identical(userCoordinate, _unset)
          ? this.userCoordinate
          : userCoordinate as LatLng?,
      reviewDraft: reviewDraft ?? this.reviewDraft,
      voiceFilterTranscript:
          voiceFilterTranscript ?? this.voiceFilterTranscript,
      voiceFilterTags: voiceFilterTags ?? this.voiceFilterTags,
    );
  }
}

class PlaceViewState {
  const PlaceViewState({
    required this.status,
    required this.placeState,
    required this.errorMessage,
  });

  const PlaceViewState.loading()
    : status = PlaceLoadStatus.loading,
      placeState = null,
      errorMessage = null;

  const PlaceViewState.ready(PlaceState state)
    : status = PlaceLoadStatus.ready,
      placeState = state,
      errorMessage = null;

  const PlaceViewState.failure(String message)
    : status = PlaceLoadStatus.failure,
      placeState = null,
      errorMessage = message;

  final PlaceLoadStatus status;
  final PlaceState? placeState;
  final String? errorMessage;
}

class PlaceCubit extends Cubit<PlaceViewState> {
  PlaceCubit({
    required AuthRepository authRepository,
    required PlacesRepository placesRepository,
    required VoiceExperiencesRepository voiceExperiencesRepository,
    required CurrentLocation location,
  }) : _authRepository = authRepository,
       _placesRepository = placesRepository,
       _voiceExperiencesRepository = voiceExperiencesRepository,
       _location = location,
       super(const PlaceViewState.loading());

  final AuthRepository _authRepository;
  final PlacesRepository _placesRepository;
  final VoiceExperiencesRepository _voiceExperiencesRepository;
  final CurrentLocation _location;

  Future<void> load() async {
    emit(const PlaceViewState.loading());

    if (!_authRepository.hasTelegramAuth) {
      emit(PlaceViewState.ready(_emptyState(hasTelegramAuth: false)));
      return;
    }

    final results = await Future.wait<Object?>([
      _authRepository.authenticateTelegram(),
      _placesRepository.fetchPlaces(),
    ]);
    final currentUser = results[0] as AppUser;
    final places = results[1] as List<PlaceRecommendation>;
    emit(
      PlaceViewState.ready(
        PlaceState(
          selectedTrait: null,
          places: places,
          selectedPlaceId: null,
          currentUser: currentUser,
          hasTelegramAuth: _authRepository.hasTelegramAuth,
          userCoordinate: null,
          reviewDraft: _emptyDraft,
          voiceFilterTranscript: '',
          voiceFilterTags: const [],
        ),
      ),
    );
    unawaited(_resolveLocation());
  }

  void selectTrait(PlaceTrait trait) {
    final value = _requireReady();
    PlaceRecommendation? next;
    for (final place in value.places) {
      if (place.traits.contains(trait)) {
        next = place;
        break;
      }
    }
    emit(
      PlaceViewState.ready(
        value.copyWith(
          selectedTrait: trait,
          selectedPlaceId: next?.id,
          voiceFilterTranscript: '',
          voiceFilterTags: const [],
        ),
      ),
    );
  }

  void clearTrait() {
    final value = _requireReady();
    emit(
      PlaceViewState.ready(
        value.copyWith(selectedTrait: null, selectedPlaceId: null),
      ),
    );
  }

  void selectPlace(String placeId) {
    final value = _requireReady();
    emit(PlaceViewState.ready(value.copyWith(selectedPlaceId: placeId)));
  }

  Future<void> recommendPlacesByVoice({
    required String audioContentBase64,
    required String audioMimeType,
  }) async {
    final value = _requireReady();
    final result = await _placesRepository.recommendPlacesByVoice(
      audioContentBase64: audioContentBase64,
      audioMimeType: audioMimeType,
    );
    emit(
      PlaceViewState.ready(
        value.copyWith(
          places: result.places,
          selectedPlaceId: result.places.isEmpty
              ? null
              : result.places.first.id,
          selectedTrait: null,
          voiceFilterTranscript: result.transcript,
          voiceFilterTags: result.tags,
        ),
      ),
    );
  }

  Future<void> clearVoiceFilter() async {
    final value = _requireReady();
    final places = await _placesRepository.fetchPlaces();
    emit(
      PlaceViewState.ready(
        value.copyWith(
          places: places,
          selectedPlaceId: places.isEmpty ? null : places.first.id,
          selectedTrait: null,
          voiceFilterTranscript: '',
          voiceFilterTags: const [],
        ),
      ),
    );
  }

  Future<PlaceRecommendation> loadPlaceDetails(
    PlaceRecommendation place,
  ) async {
    if (place.photoUrls.isNotEmpty ||
        place.googleCurrentOpeningHours != null ||
        place.googleRegularOpeningHours != null) {
      return place;
    }

    final updated = await _placesRepository.fetchPlace(place.id);
    final value = _requireReady();
    emit(
      PlaceViewState.ready(
        value.copyWith(
          places: _replacePlace(updated),
          selectedPlaceId: updated.id,
        ),
      ),
    );
    return updated;
  }

  Future<void> toggleFavorite(PlaceRecommendation place) async {
    final value = _requireReady();
    final updated = place.isFavorite
        ? await _placesRepository.removeFavoritePlace(place.id)
        : await _placesRepository.addFavoritePlace(place.id);
    emit(PlaceViewState.ready(value.copyWith(places: _replacePlace(updated))));
  }

  void setReviewPlace(String placeName) {
    final value = _requireReady();
    emit(
      PlaceViewState.ready(
        value.copyWith(
          reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
        ),
      ),
    );
  }

  void setReviewDuration(Duration duration) {
    final value = _requireReady();
    emit(
      PlaceViewState.ready(
        value.copyWith(
          reviewDraft: value.reviewDraft.copyWith(duration: duration),
        ),
      ),
    );
  }

  Future<void> publishReview({
    required PlaceRecommendation place,
    required String audioObjectKey,
    required String audioContentBase64,
    required String audioMimeType,
    required bool addToFavorites,
    required Map<String, dynamic> recordingPayload,
    required List<String> promptHintsShown,
  }) async {
    final value = _requireReady();
    if (!value.hasTelegramAuth) {
      throw StateError('Открой через Telegram, чтобы оставить голос.');
    }

    final draft = value.reviewDraft;
    await _voiceExperiencesRepository.createVoiceExperience(
      googlePlaceId: place.googlePlaceId,
      googleName: place.name,
      coordinate: place.coordinate,
      googlePrimaryType: place.googlePrimaryType,
      googleTypes: place.googleTypes,
      durationSeconds: draft.duration.inSeconds,
      audioObjectKey: audioObjectKey,
      audioContentBase64: audioContentBase64,
      audioMimeType: audioMimeType,
      addToFavorites: addToFavorites,
      recordingPayload: recordingPayload,
      promptHintsShown: promptHintsShown,
    );

    final places = await _placesRepository.fetchPlaces();
    emit(
      PlaceViewState.ready(
        value.copyWith(
          places: places,
          selectedPlaceId: null,
          reviewDraft: _emptyDraft,
          voiceFilterTranscript: '',
          voiceFilterTags: const [],
        ),
      ),
    );
  }

  List<PlaceRecommendation> _replacePlace(PlaceRecommendation updated) {
    final value = _requireReady();
    return [
      for (final place in value.places)
        if (place.id == updated.id) updated else place,
    ];
  }

  Future<void> _resolveLocation() async {
    final coordinate = await _location.resolve().catchError((Object _) => null);
    final value = state.placeState;
    if (value == null || coordinate == null) {
      return;
    }
    emit(PlaceViewState.ready(value.copyWith(userCoordinate: coordinate)));
  }

  PlaceState _requireReady() {
    final value = state.placeState;
    if (value == null) {
      throw StateError('Place state is not ready.');
    }
    return value;
  }

  static PlaceState _emptyState({required bool hasTelegramAuth}) {
    return PlaceState(
      selectedTrait: null,
      places: const [],
      selectedPlaceId: null,
      currentUser: null,
      hasTelegramAuth: hasTelegramAuth,
      userCoordinate: null,
      reviewDraft: _emptyDraft,
      voiceFilterTranscript: '',
      voiceFilterTags: const [],
    );
  }
}

const _emptyDraft = VoiceReviewDraft(
  placeName: '',
  duration: Duration.zero,
  extractedTraits: {},
  evidence: [],
);
