

library;

import '../../core/contracts/library.dart';
import '../../shared/utils/time_bucket.dart';


enum GroupAxis { watchedTime, addedTime, mediaType }


class AggregatedEntrySource {
  final EmbyRawItem item;
  final GlobalSearchServerResult source;

  const AggregatedEntrySource({required this.item, required this.source});


  DateTime? dateFor(GroupAxis axis) {
    switch (axis) {
      case GroupAxis.watchedTime:
        final userData = item.extra['UserData'];
        return parseEmbyDate(
          userData is Map ? userData['LastPlayedDate'] : null,
        );
      case GroupAxis.addedTime:
        return parseEmbyDate(item.extra['DateCreated']);
      case GroupAxis.mediaType:
        return null;
    }
  }
}


class AggregatedEntry {
  final EmbyRawItem item;
  final GlobalSearchServerResult source;
  final List<AggregatedEntrySource> sources;

  AggregatedEntry({
    required this.item,
    required this.source,
    List<AggregatedEntrySource>? sources,
  }) : assert(sources == null || sources.isNotEmpty),
       sources = List.unmodifiable(
         sources ?? [AggregatedEntrySource(item: item, source: source)],
       );


  DateTime? dateFor(GroupAxis axis) {
    return sources.first.dateFor(axis);
  }
}


class AggregatedGroup {
  final String key;
  final String label;
  final int order;
  final List<AggregatedEntry> entries;

  const AggregatedGroup({
    required this.key,
    required this.label,
    required this.order,
    required this.entries,
  });
}


List<AggregatedEntry> flatten(
  List<GlobalSearchServerResult> results, {
  String? serverFilter,
  bool deduplicateAcrossServers = false,
}) {
  final out = <AggregatedEntry>[];
  for (final sr in results) {
    if (serverFilter != null && sr.serverId != serverFilter) continue;
    for (final item in sr.items) {
      out.add(AggregatedEntry(item: item, source: sr));
    }
  }
  if (deduplicateAcrossServers && serverFilter == null && out.length > 1) {
    return _deduplicateEntries(out);
  }
  return out;
}

const _typeOrder = <String, ({String label, int order})>{
  'Movie': (label: '电影', order: 0),
  'Series': (label: '剧集', order: 1),
  'Episode': (label: '单集', order: 2),
};
const _typeOther = (label: '其它', order: 3);


List<AggregatedGroup> groupEntries(
  List<AggregatedEntry> entries,
  GroupAxis axis,
  DateTime now,
) {
  if (axis == GroupAxis.mediaType) {
    return _groupByType(entries);
  }
  return _groupByTime(entries, axis, now);
}

List<AggregatedGroup> _groupByTime(
  List<AggregatedEntry> entries,
  GroupAxis axis,
  DateTime now,
) {
  final scheme = axis == GroupAxis.watchedTime
      ? TimeBucketScheme.watched
      : TimeBucketScheme.added;
  final buckets =
      <String, ({TimeBucket bucket, List<AggregatedEntry> items})>{};

  for (final e in entries) {
    final b = bucketFor(e.dateFor(axis), now, scheme);
    (buckets[b.key] ??= (bucket: b, items: [])).items.add(e);
  }

  final groups = buckets.values.map((v) {
    final items = [...v.items]
      ..sort((a, b) {
        final da = a.dateFor(axis);
        final db = b.dateFor(axis);
        if (da == null && db == null) return 0;
        if (da == null) return 1;
        if (db == null) return -1;
        return db.compareTo(da);
      });
    return AggregatedGroup(
      key: v.bucket.key,
      label: v.bucket.label,
      order: v.bucket.order,
      entries: items,
    );
  }).toList()..sort((a, b) => a.order.compareTo(b.order));
  return groups;
}

List<AggregatedGroup> _groupByType(List<AggregatedEntry> entries) {
  final groups =
      <String, ({String label, int order, List<AggregatedEntry> items})>{};

  for (final e in entries) {
    final meta = _typeOrder[e.item.Type] ?? _typeOther;
    final key = e.item.Type ?? 'Other';
    (groups[key] ??= (
      label: meta.label,
      order: meta.order,
      items: [],
    )).items.add(e);
  }

  final out = groups.entries.map((kv) {
    final items = [...kv.value.items]
      ..sort((a, b) => (a.item.Name).compareTo(b.item.Name));
    return AggregatedGroup(
      key: kv.key,
      label: kv.value.label,
      order: kv.value.order,
      entries: items,
    );
  }).toList()..sort((a, b) => a.order.compareTo(b.order));
  return out;
}

Set<String> _candidateKeys(EmbyRawItem item) {
  final type = item.Type;
  if (type == null || type.isEmpty) return const {};
  final ids = normalizedProviderIdsOfItem(item);
  if (ids.isEmpty) return const {};
  return {for (final e in ids.entries) '$type:${e.key}:${e.value}'};
}

List<AggregatedEntry> _deduplicateEntries(List<AggregatedEntry> entries) {
  final uf = _UnionFind(entries.length);
  final keyToIndex = <String, int>{};

  for (var i = 0; i < entries.length; i++) {
    final keys = _candidateKeys(entries[i].item);
    for (final k in keys) {
      final prev = keyToIndex[k];
      if (prev != null) {
        uf.union(prev, i);
      } else {
        keyToIndex[k] = i;
      }
    }
  }

  final groups = uf.groups();
  final survivors = <AggregatedEntry>[];
  for (final group in groups) {
    final orderedIndices = [...group]
      ..sort((leftIndex, rightIndex) {
        final leftDate = _lastPlayedDate(entries[leftIndex]);
        final rightDate = _lastPlayedDate(entries[rightIndex]);
        if (leftDate == null && rightDate == null) {
          return leftIndex.compareTo(rightIndex);
        }
        if (leftDate == null) return 1;
        if (rightDate == null) return -1;

        final dateComparison = rightDate.compareTo(leftDate);
        return dateComparison != 0
            ? dateComparison
            : leftIndex.compareTo(rightIndex);
      });

    final sourceServerIds = <String>{};
    final sources = <AggregatedEntrySource>[];
    for (final entryIndex in orderedIndices) {
      final entry = entries[entryIndex];
      if (!sourceServerIds.add(entry.source.serverId)) continue;
      sources.add(
        AggregatedEntrySource(item: entry.item, source: entry.source),
      );
    }

    final primarySource = sources.first;
    survivors.add(
      AggregatedEntry(
        item: primarySource.item,
        source: primarySource.source,
        sources: sources,
      ),
    );
  }
  return survivors;
}

DateTime? _lastPlayedDate(AggregatedEntry e) =>
    e.dateFor(GroupAxis.watchedTime);

class _UnionFind {
  final List<int> _parent;
  final List<int> _rank;

  _UnionFind(int n)
    : _parent = List.generate(n, (i) => i),
      _rank = List.filled(n, 0);

  int find(int x) {
    if (_parent[x] != x) _parent[x] = find(_parent[x]);
    return _parent[x];
  }

  void union(int a, int b) {
    final ra = find(a), rb = find(b);
    if (ra == rb) return;
    if (_rank[ra] < _rank[rb]) {
      _parent[ra] = rb;
    } else if (_rank[ra] > _rank[rb]) {
      _parent[rb] = ra;
    } else {
      _parent[rb] = ra;
      _rank[ra]++;
    }
  }

  List<List<int>> groups() {
    final map = <int, List<int>>{};
    for (var i = 0; i < _parent.length; i++) {
      (map[find(i)] ??= []).add(i);
    }
    return map.values.toList();
  }
}
