import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/covered/tag.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/locales.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/styles.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/view/src/metadata/exportable_fields.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/action_mixins/vault_aware.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/basic/tv_edge_focus.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/dialogs/export_collection_stats_page.dart';
import 'package:aves/widgets/stats/date/histogram.dart';
import 'package:aves/widgets/stats/filter_table.dart';
import 'package:aves/widgets/stats/mime_donut.dart';
import 'package:aves/widgets/stats/percent_text.dart';
import 'package:aves/widgets/stats/top_page.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:csv/csv.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';

class StatsPage extends StatefulWidget {
  static const routeName = '/collection/stats';

  final Set<AvesEntry> entries;
  final CollectionSource source;
  final CollectionLens? parentCollection;

  const StatsPage({
    super.key,
    required this.entries,
    required this.source,
    this.parentCollection,
  });

  @override
  State<StatsPage> createState() => _StatsPageState();
}

class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMixin {
  final Map<String, int> _entryCountPerCountry = {}, _entryCountPerState = {}, _entryCountPerPlace = {};
  final Map<String, int> _entryCountPerTag = {}, _entryCountPerAlbum = {};
  final Map<int, int> _entryCountPerRating = Map.fromEntries(List.generate(7, (i) => MapEntry(5 - i, 0)));
  late final ValueNotifier<bool> _isPageAnimatingNotifier;
  int _totalSizeBytes = 0;
  int _totalDurationMillis = 0;

  Set<AvesEntry> get entries => widget.entries;

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

    _isPageAnimatingNotifier = ValueNotifier(true);
    Future.delayed(ADurations.pageTransitionLoose * timeDilation).then((_) {
      if (!mounted) return;
      _isPageAnimatingNotifier.value = false;
    });

    entries.forEach((entry) {
      _totalSizeBytes += entry.sizeBytes ?? 0;
      _totalDurationMillis += entry.durationMillis ?? 0;

      if (entry.hasAddress) {
        final address = entry.addressDetails!;
        var country = address.countryName;
        if (country != null && country.isNotEmpty) {
          country += '${LocationFilter.locationSeparator}${address.countryCode}';
          _entryCountPerCountry[country] = (_entryCountPerCountry[country] ?? 0) + 1;
        }
        var state = address.stateName;
        if (state != null && state.isNotEmpty) {
          state += '${LocationFilter.locationSeparator}${address.stateCode}';
          _entryCountPerState[state] = (_entryCountPerState[state] ?? 0) + 1;
        }
        final place = address.place;
        if (place != null && place.isNotEmpty) {
          _entryCountPerPlace[place] = (_entryCountPerPlace[place] ?? 0) + 1;
        }
      }

      entry.tags.forEach((tag) {
        _entryCountPerTag[tag] = (_entryCountPerTag[tag] ?? 0) + 1;
      });

      final album = entry.directory;
      if (album != null) {
        _entryCountPerAlbum[album] = (_entryCountPerAlbum[album] ?? 0) + 1;
      }

      final rating = entry.rating;
      _entryCountPerRating[rating] = (_entryCountPerRating[rating] ?? 0) + 1;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final useTvLayout = settings.useTvLayout;
    return ValueListenableBuilder<bool>(
      valueListenable: _isPageAnimatingNotifier,
      builder: (context, animating, child) {
        final l10n = context.l10n;

        Widget child = const SizedBox();

        if (!animating) {
          final durations = context.watch<DurationsData>();

          if (entries.isEmpty) {
            child = EmptyContent(
              icon: AIcons.image,
              text: l10n.collectionEmptyImages,
            );
          } else {
            final chartAnimationDuration = context.read<DurationsData>().chartTransition;

            final byMimeTypes = groupBy<AvesEntry, String>(entries, (entry) => entry.mimeType).map<String, int>((k, v) => MapEntry(k, v.length));
            final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image')));
            final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video')));
            final mimeDonuts = Wrap(
              alignment: WrapAlignment.center,
              crossAxisAlignment: WrapCrossAlignment.center,
              children: [
                MimeDonut(
                  icon: AIcons.image,
                  byMimeTypes: imagesByMimeTypes,
                  animationDuration: chartAnimationDuration,
                  onFilterSelection: (filter) => _onFilterSelection(context, filter),
                ),
                MimeDonut(
                  icon: AIcons.video,
                  byMimeTypes: videoByMimeTypes,
                  animationDuration: chartAnimationDuration,
                  onFilterSelection: (filter) => _onFilterSelection(context, filter),
                ),
              ],
            );

            final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0);
            final source = widget.source;
            child = NotificationListener<SelectFilterNotification>(
              onNotification: (notification) {
                _onFilterSelection(context, notification.filter);
                return true;
              },
              child: AnimationLimiter(
                child: ListView(
                  children: AnimationConfiguration.toStaggeredList(
                    duration: durations.staggeredAnimation,
                    delay: durations.staggeredAnimationDelay * timeDilation,
                    childAnimationBuilder: (child) => SlideAnimation(
                      verticalOffset: 50.0,
                      child: FadeInAnimation(
                        child: child,
                      ),
                    ),
                    children: [
                      const TvEdgeFocus(),
                      mimeDonuts,
                      Histogram(
                        entries: entries,
                        animationDuration: chartAnimationDuration,
                        onFilterSelection: (filter) => _onFilterSelection(context, filter),
                      ),
                      const SizedBox(height: 16),
                      _buildTotalSizeIndicator(context),
                      if (_totalDurationMillis > 0) ...[
                        const SizedBox(height: 16),
                        _buildTotalDurationIndicator(),
                      ],
                      const SizedBox(height: 16),
                      _LocationIndicator(entries: entries),
                      ..._buildFilterSection<String>(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)),
                      ..._buildFilterSection<String>(context, l10n.statsTopStatesSectionTitle, _entryCountPerState, (v) => LocationFilter(LocationLevel.state, v)),
                      ..._buildFilterSection<String>(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)),
                      ..._buildFilterSection<String>(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new),
                      ..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => StoredAlbumFilter(v, source.getStoredAlbumDisplayName(context, v))),
                      if (showRatings) ..._buildFilterSection<int>(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null),
                    ],
                  ),
                ),
              ),
            );
          }
        }

        return AvesScaffold(
          appBar: AppBar(
            automaticallyImplyLeading: !useTvLayout,
            title: Text(l10n.statsPageTitle),
            actions: [
              IconButton(
                icon: Icon(AIcons.fileExport),
                onPressed: () => _export(context),
                tooltip: context.l10n.settingsActionExport,
              ),
            ],
          ),
          body: GestureAreaProtectorStack(
            child: SafeArea(
              bottom: false,
              child: TooltipTheme(
                data: TooltipTheme.of(context).copyWith(
                  preferBelow: false,
                ),
                child: child,
              ),
            ),
          ),
        );
      },
    );
  }

  Widget _buildTotalSizeIndicator(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(AIcons.size),
          const SizedBox(width: 16),
          Expanded(
            child: Text(formatFileSize(context.locale, _totalSizeBytes)),
          ),
        ],
      ),
    );
  }

  Widget _buildTotalDurationIndicator() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(AIcons.videoPlay),
          const SizedBox(width: 16),
          Expanded(
            child: Text(formatFriendlyDuration(Duration(milliseconds: _totalDurationMillis))),
          ),
        ],
      ),
    );
  }

  List<Widget> _buildFilterSection<T extends Comparable>(
    BuildContext context,
    String title,
    Map<T, int> entryCountMap,
    CollectionFilter Function(T key) filterBuilder, {
    bool sortByCount = true,
    int? maxRowCount = 3,
  }) {
    if (entryCountMap.isEmpty) return [];

    final totalEntryCount = entries.length;
    final hasMore = maxRowCount != null && entryCountMap.length > maxRowCount;
    final onHeaderPressed = hasMore
        ? () => Navigator.maybeOf(context)?.push(
            MaterialPageRoute(
              settings: const RouteSettings(name: StatsTopPage.routeName),
              builder: (context) => StatsTopPage(
                title: title,
                totalEntryCount: totalEntryCount,
                entryCountMap: entryCountMap,
                filterBuilder: filterBuilder,
                sortByCount: sortByCount,
                onFilterSelection: (filter) => _onFilterSelection(context, filter),
              ),
            ),
          )
        : null;
    Widget header = Text(
      title,
      style: AStyles.knownTitleText,
    );
    if (settings.useTvLayout) {
      header = Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            header,
            const SizedBox(width: 16),
            Icon(AIcons.next, color: hasMore ? null : Theme.of(context).disabledColor),
          ],
        ),
      );
      header = Container(
        padding: const EdgeInsets.symmetric(vertical: 12),
        alignment: AlignmentDirectional.centerStart,
        // prevent ink response when tapping the header does nothing,
        // because otherwise Play Store reviewers think it is broken navigation
        child: onHeaderPressed != null
            ? InkWell(
                onTap: onHeaderPressed,
                borderRadius: const BorderRadius.all(Radius.circular(123)),
                child: header,
              )
            : Focus(child: header),
      );
    } else {
      header = Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            header,
            const Spacer(),
            IconButton(
              icon: const Icon(AIcons.next),
              onPressed: onHeaderPressed,
              tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
            ),
          ],
        ),
      );
    }

    return [
      header,
      FilterTable(
        totalEntryCount: totalEntryCount,
        entryCountMap: entryCountMap,
        filterBuilder: filterBuilder,
        sortByCount: sortByCount,
        maxRowCount: maxRowCount,
        onFilterSelection: (filter) => _onFilterSelection(context, filter),
      ),
    ];
  }

  Future<void> _onFilterSelection(BuildContext context, CollectionFilter filter) async {
    if (!await unlockFilter(context, filter)) return;

    if (widget.parentCollection != null) {
      _applyToParentCollectionPage(context, filter);
    } else {
      _jumpToCollectionPage(context, filter);
    }
  }

  void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
    widget.parentCollection!.addFilters({filter});
    // We delay closing the current page after applying the filter selection
    // so that hero animation target is ready in the `FilterBar`,
    // even when the target is a child of an `AnimatedList`.
    // Do not use `WidgetsBinding.instance.addPostFrameCallback`,
    // as it may not trigger if there is no subsequent build.
    Future.delayed(const Duration(milliseconds: 100), () => Navigator.maybeOf(context)?.popUntil((route) => route.settings.name == CollectionPage.routeName));
  }

  void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) {
    Navigator.maybeOf(context)?.pushAndRemoveUntil(
      MaterialPageRoute(
        settings: const RouteSettings(name: CollectionPage.routeName),
        builder: (context) => CollectionPage(
          source: widget.source,
          filters: {filter},
        ),
      ),
      (route) => false,
    );
  }

  Future<void> _export(BuildContext context) async {
    final sample = entries.first;
    final locale = context.locale;
    String previewer(ExportableEntryField field) => _exportEntryField(field, sample, locale)?.toString() ?? '';

    final options = await Navigator.maybeOf(context)?.push<(String, Set<ExportableEntryField>, ExportTarget)>(
      MaterialPageRoute(
        settings: const RouteSettings(name: ExportCollectionStatsPage.routeName),
        builder: (context) => ExportCollectionStatsPage(previewer: previewer),
      ),
    );
    if (options == null) return;

    final (mimeType, fieldSet, target) = options;
    final index = ExportableEntryField.values.indexOf;
    final fieldList = fieldSet.sorted((a, b) => index(a).compareTo(index(b)));

    String body = '';
    switch (mimeType) {
      case MimeTypes.csv:
        body = _exportToCsv(fieldList, context);
      case MimeTypes.json:
        body = _exportToJson(fieldList);
    }

    final bool? success;
    switch (target) {
      case ExportTarget.clipboard:
        success = await appService.copyToClipboard(text: body);
      case ExportTarget.file:
        success = await storageService.createFile(
          'aves-stats-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}${MimeTypes.extensionFor(mimeType)}',
          mimeType,
          Uint8List.fromList(utf8.encode(body)),
        );
    }
    if (success != null) {
      if (success) {
        showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
      } else {
        showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
      }
    }
  }

  static Object? _exportEntryField(ExportableEntryField field, AvesEntry entry, String locale) {
    switch (field) {
      case ExportableEntryField.uri:
        return entry.uri;
      case ExportableEntryField.path:
        return entry.path;
      case ExportableEntryField.title:
        return entry.bestTitle;
      case ExportableEntryField.date:
        return entry.bestDate?.toIso8601String();
      case ExportableEntryField.size:
        return entry.sizeBytes;
      case ExportableEntryField.resolution:
        return entry.getResolutionText(locale);
      case ExportableEntryField.width:
        return entry.displaySize.width.toInt();
      case ExportableEntryField.height:
        return entry.displaySize.height.toInt();
      case ExportableEntryField.duration:
        final durationMillis = entry.durationMillis ?? 0;
        return durationMillis > 0 ? durationMillis : null;
      case ExportableEntryField.coordinates:
        final latLng = entry.latLng;
        return latLng != null ? '${latLng.latitude},${latLng.longitude}' : null;
      case ExportableEntryField.address:
        final shortAddress = entry.shortAddress;
        return shortAddress.isNotEmpty ? shortAddress : null;
      case ExportableEntryField.tags:
        return entry.tags.join(';');
    }
  }

  String _exportToCsv(List<ExportableEntryField> fields, BuildContext context) {
    final locale = context.locale;
    final headers = fields.map((v) => v.getText(context)).toList();
    List<String> toCsvValues(AvesEntry entry) => fields.map((field) {
      return _exportEntryField(field, entry, locale)?.toString() ?? '';
    }).toList();
    return const ListToCsvConverter().convert(
       [headers, ...entries.map(toCsvValues)],
    );
  }

  String _exportToJson(List<ExportableEntryField> fields) {
    final locale = context.locale;
    Map<String, Object?> toJsonMap(AvesEntry entry) => Map.fromEntries(
      fields.map((field) => MapEntry(field.name, _exportEntryField(field, entry, locale))),
    );
    return jsonEncode(entries.map(toJsonMap).toList());
  }
}

class _LocationIndicator extends StatelessWidget {
  final Set<AvesEntry> entries;

  const _LocationIndicator({required this.entries});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final catalogued = entries.where((entry) => entry.isCatalogued);
    final withGps = catalogued.where((entry) => entry.hasGps);
    final withGpsCount = withGps.length;
    final withGpsPercent = withGpsCount / entries.length;
    final textScaler = MediaQuery.textScalerOf(context);
    final lineHeight = textScaler.scale(16);
    final barRadius = Radius.circular(lineHeight / 2);
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Column(
        children: [
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(AIcons.location),
              Expanded(
                child: Stack(
                  // use a stack instead of `center` field, so that the widgets
                  // are centered even when the center child has larger height
                  alignment: Alignment.center,
                  children: [
                    LinearPercentIndicator(
                      percent: withGpsPercent,
                      lineHeight: lineHeight,
                      backgroundColor: Themes.secondLayerColor(context),
                      progressColor: theme.colorScheme.primary,
                      animation: context.select<Settings, bool>((v) => v.animate),
                      isRTL: context.isRtl,
                      barRadius: barRadius,
                      padding: EdgeInsets.symmetric(horizontal: lineHeight),
                    ),
                    LinearPercentIndicatorText(percent: withGpsPercent),
                  ],
                ),
              ),
              // end padding to match leading, so that inside label is aligned with outside label below
              const SizedBox(width: 24),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            context.l10n.statsWithGps(withGpsCount),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}
