import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';

import '../../../../api/errors/error_mapper.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items.dart';
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items_overview.dart';
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items_overview_response.dart';
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items_response.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../extensions/date_time.dart';
import '../../../../model/account_data.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../share_intent/remote_file_ref.dart';
import '../../../../utils/downloads/download_job.dart';
import '../../../../widget/app_progress_indicator.dart';
import '../../../../widget/demo_restricted.dart';
import '../../../../widget/downloads/download_trigger.dart';

const int _sharedItemsPageSize = 20;

/// The non-media shared-item categories Talk exposes, in tab order. `media` is
/// handled separately (split into Bilder/Videos). `location`/`deckcard` are rich
/// objects without a downloadable file, so they have no place in this view.
const List<_SharedCategory> _sharedCategories = [
  _SharedCategory('file', 'Dokumente', _SharedItemsLayout.list),
  _SharedCategory('voice', 'Sprachnachrichten', _SharedItemsLayout.list),
  _SharedCategory('audio', 'Audio', _SharedItemsLayout.list),
  _SharedCategory('recording', 'Aufnahmen', _SharedItemsLayout.list),
  _SharedCategory('other', 'Sonstiges', _SharedItemsLayout.list),
];

class _SharedCategory {
  final String objectType;
  final String label;
  final _SharedItemsLayout layout;

  const _SharedCategory(this.objectType, this.label, this.layout);
}

bool _isVideoItem(GetChatResponseObject item) {
  final file = item.messageParameters?['file'];
  return file != null && _isVideoFile(file.name);
}

/// One page of shared items plus the pagination cursor for the next request.
/// Produced by [buildSharedItemsPage] so the in-view loader and the overview
/// seed share the exact same paging semantics.
class SharedItemsPage {
  final List<GetChatResponseObject> items;
  final int? lastKnownMessageId;
  final bool hasMore;

  const SharedItemsPage(this.items, this.lastKnownMessageId, this.hasMore);
}

List<GetChatResponseObject> _fileItems(List<GetChatResponseObject> items) => items
    .where((item) => item.messageParameters?['file']?.path != null)
    .toList();

SharedItemsPage buildSharedItemsPage(
  GetSharedItemsResponse response,
  int? previousMessageId,
) {
  final lastGiven = response.lastGivenMessageId;
  final hasMore =
      response.items.length >= _sharedItemsPageSize &&
      lastGiven != null &&
      lastGiven != previousMessageId;
  return SharedItemsPage(
    _fileItems(response.items),
    hasMore ? lastGiven : previousMessageId,
    hasMore,
  );
}

/// Seeds a tab from the overview payload. The pagination cursor is the oldest
/// message id already shown; the per-type endpoint takes over from there via
/// its `X-Chat-Last-Given` header. [hasMore] is a heuristic — a full page from
/// the overview means there are probably older items to fetch on scroll.
SharedItemsPage _seedFromOverview(List<GetChatResponseObject> rawItems) {
  final hasMore = rawItems.length >= _sharedItemsPageSize;
  final oldestId = rawItems.isEmpty
      ? null
      : rawItems.map((item) => item.id).reduce((a, b) => a < b ? a : b);
  return SharedItemsPage(
    _fileItems(rawItems),
    hasMore ? oldestId : null,
    hasMore,
  );
}

/// WhatsApp-style overview of everything shared in a chat. One tab per Talk
/// share category, but only categories that actually contain a downloadable
/// file are shown. Each tab paginates on its own via [GetSharedItems].
class SharedItemsView extends StatefulWidget {
  final GetRoomResponseObject room;

  /// Overview fetched ahead of time (by ChatInfo) so tabs render instantly.
  /// Null => this view fetches it itself on open.
  final GetSharedItemsOverviewResponse? overview;

  const SharedItemsView(this.room, {this.overview, super.key});

  /// Best-effort preload used by ChatInfo while the user is on the details
  /// screen, so opening this view needs no round-trip.
  static Future<GetSharedItemsOverviewResponse> prefetchOverview(
    String token,
  ) => GetSharedItemsOverview(token, limit: _sharedItemsPageSize).run();

  @override
  State<SharedItemsView> createState() => _SharedItemsViewState();
}

class _SharedItemsViewState extends State<SharedItemsView>
    with SingleTickerProviderStateMixin {
  GetSharedItemsOverviewResponse? _overview;
  Object? _error;

  List<(String, Widget)>? _tabs;
  TabController? _tabController;

  @override
  void initState() {
    super.initState();
    _overview = widget.overview;
    if (_overview == null) {
      _load();
    } else {
      _prepareTabs();
    }
  }

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

  Future<void> _load() async {
    setState(() => _error = null);
    try {
      final overview = await SharedItemsView.prefetchOverview(widget.room.token);
      if (!mounted) return;
      _overview = overview;
      _prepareTabs();
      setState(() {});
    } catch (e) {
      if (mounted) setState(() => _error = e);
    }
  }

  /// Builds the tabs and their controller exactly once. A scrollable [TabBar]
  /// crashes with "setState during build" when an ancestor rebuild swaps the
  /// [TabController] out mid-fling; owning a stable controller here avoids that.
  void _prepareTabs() {
    final overview = _overview;
    if (overview == null) return;

    final tabs = <(String, Widget)>[];

    // Media is one server category but two tabs: split the shared stream into
    // Bilder/Videos client-side. Both tabs page the same `media` endpoint.
    // Videos have no server preview here, so they render as a name list.
    final mediaSeed = _seedFromOverview(
      overview.itemsByType['media'] ?? const [],
    );
    if (mediaSeed.items.any((item) => !_isVideoItem(item))) {
      tabs.add((
        'Bilder',
        _SharedItemsTab(
          token: widget.room.token,
          objectType: 'media',
          layout: _SharedItemsLayout.grid,
          emptyLabel: 'Keine Bilder',
          initialPage: mediaSeed,
          itemFilter: (item) => !_isVideoItem(item),
        ),
      ));
    }
    if (mediaSeed.items.any(_isVideoItem)) {
      tabs.add((
        'Videos',
        _SharedItemsTab(
          token: widget.room.token,
          objectType: 'media',
          layout: _SharedItemsLayout.list,
          emptyLabel: 'Keine Videos',
          initialPage: mediaSeed,
          itemFilter: _isVideoItem,
        ),
      ));
    }

    for (final category in _sharedCategories) {
      final raw = overview.itemsByType[category.objectType] ?? const [];
      if (_fileItems(raw).isEmpty) continue;
      tabs.add((
        category.label,
        _SharedItemsTab(
          token: widget.room.token,
          objectType: category.objectType,
          layout: category.layout,
          emptyLabel: 'Keine Einträge',
          initialPage: _seedFromOverview(raw),
        ),
      ));
    }

    _tabs = tabs;
    _tabController = tabs.isEmpty
        ? null
        : TabController(length: tabs.length, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    if (_overview == null) {
      return Scaffold(
        appBar: AppBar(title: const Text('Medien und Dokumente')),
        body: _error != null
            ? _ErrorState(message: errorToUserMessage(_error), onRetry: _load)
            : const Center(child: AppProgressIndicator.medium()),
      );
    }

    final tabs = _tabs ?? const [];
    if (tabs.isEmpty) {
      return Scaffold(
        appBar: AppBar(title: const Text('Medien und Dokumente')),
        body: Center(
          child: Text(
            'Noch nichts geteilt',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ),
      );
    }

    final scrollable = tabs.length > 3;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Medien und Dokumente'),
        bottom: TabBar(
          controller: _tabController,
          isScrollable: scrollable,
          tabAlignment: scrollable ? TabAlignment.start : TabAlignment.fill,
          tabs: [for (final tab in tabs) Tab(text: tab.$1)],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [for (final tab in tabs) tab.$2],
      ),
    );
  }
}

enum _SharedItemsLayout { grid, list }

class _SharedItemsTab extends StatefulWidget {
  final String token;
  final String objectType;
  final _SharedItemsLayout layout;
  final String emptyLabel;
  final SharedItemsPage? initialPage;

  /// Optional client-side filter. Used to carve the combined `media` stream
  /// into separate "Bilder" and "Videos" tabs; the pagination cursor still
  /// tracks the full (unfiltered) stream.
  final bool Function(GetChatResponseObject)? itemFilter;

  const _SharedItemsTab({
    required this.token,
    required this.objectType,
    required this.layout,
    required this.emptyLabel,
    this.initialPage,
    this.itemFilter,
  });

  @override
  State<_SharedItemsTab> createState() => _SharedItemsTabState();
}

class _SharedItemsTabState extends State<_SharedItemsTab>
    with AutomaticKeepAliveClientMixin {
  // When a filtered tab keeps pulling pages that contain none of its kind, stop
  // auto-filling after this many empty pages so a video-sparse chat does not
  // page endlessly; the user can still scroll to fetch more by hand.
  static const int _maxEmptyAutoFills = 5;

  final ScrollController _scrollController = ScrollController();
  final List<GetChatResponseObject> _items = [];

  bool _loading = false;
  bool _initialLoaded = false;
  bool _hasMore = true;
  int? _lastKnownMessageId;
  int _emptyAutoFills = 0;
  Object? _error;

  @override
  bool get wantKeepAlive => true;

  List<GetChatResponseObject> _applyFilter(List<GetChatResponseObject> items) =>
      widget.itemFilter == null
      ? items
      : items.where(widget.itemFilter!).toList();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
    final prefetch = widget.initialPage;
    if (prefetch != null) {
      _items.addAll(_applyFilter(prefetch.items));
      _lastKnownMessageId = prefetch.lastKnownMessageId;
      _hasMore = prefetch.hasMore;
      _initialLoaded = true;
      _scheduleFillCheck();
    } else {
      _loadMore();
    }
  }

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

  void _onScroll() {
    if (!_scrollController.hasClients) return;
    final position = _scrollController.position;
    if (position.pixels >= position.maxScrollExtent - 400) {
      _loadMore();
    }
  }

  /// After layout, keep loading while the (possibly filtered) content is too
  /// short to scroll — otherwise scroll-based paging could never kick in.
  void _scheduleFillCheck() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || _loading || !_hasMore) return;
      if (_emptyAutoFills >= _maxEmptyAutoFills) return;
      if (!_scrollController.hasClients) return;
      if (_scrollController.position.maxScrollExtent > 0) return;
      _loadMore();
    });
  }

  Future<void> _loadMore() async {
    if (_loading || !_hasMore) return;
    setState(() {
      _loading = true;
      _error = null;
    });
    try {
      final response = await GetSharedItems(
        widget.token,
        objectType: widget.objectType,
        limit: _sharedItemsPageSize,
        lastKnownMessageId: _lastKnownMessageId,
      ).run();
      if (!mounted) return;
      final page = buildSharedItemsPage(response, _lastKnownMessageId);
      final filtered = _applyFilter(page.items);
      setState(() {
        _items.addAll(filtered);
        _lastKnownMessageId = page.lastKnownMessageId;
        _hasMore = page.hasMore;
        _initialLoaded = true;
        _loading = false;
        _emptyAutoFills = filtered.isEmpty ? _emptyAutoFills + 1 : 0;
      });
      _scheduleFillCheck();
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _error = e;
        _loading = false;
        _initialLoaded = true;
      });
    }
  }

  Future<void> _retry() {
    setState(() {
      _hasMore = true;
      _error = null;
    });
    return _loadMore();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    if (!_initialLoaded && _loading) {
      return const Center(child: AppProgressIndicator.medium());
    }

    if (_items.isEmpty && _error != null) {
      return _ErrorState(message: errorToUserMessage(_error), onRetry: _retry);
    }

    if (_items.isEmpty) {
      return Center(
        child: Text(
          widget.emptyLabel,
          style: Theme.of(context).textTheme.bodyMedium,
        ),
      );
    }

    final trailingLoader = _loading && _hasMore
        ? const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Center(child: AppProgressIndicator.small()),
            ),
          )
        : const SliverToBoxAdapter(child: SizedBox.shrink());

    // Stop this list's scroll notifications from bubbling into the enclosing
    // TabBarView, which otherwise mis-syncs its indicator and can crash with
    // "setState during build" on a fling. Our own paging uses the controller
    // listener, not notifications, so nothing here depends on them bubbling.
    return NotificationListener<ScrollNotification>(
      onNotification: (_) => true,
      child: CustomScrollView(
        controller: _scrollController,
        slivers: [
          if (widget.layout == _SharedItemsLayout.grid)
            SliverPadding(
              padding: const EdgeInsets.all(2),
              sliver: SliverGrid(
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  mainAxisSpacing: 2,
                  crossAxisSpacing: 2,
                ),
                delegate: SliverChildBuilderDelegate(
                  (context, index) => _SharedItemTile(
                    item: _items[index],
                    layout: _SharedItemsLayout.grid,
                  ),
                  childCount: _items.length,
                ),
              ),
            )
          else
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) => _SharedItemTile(
                  item: _items[index],
                  layout: _SharedItemsLayout.list,
                ),
                childCount: _items.length,
              ),
            ),
          trailingLoader,
        ],
      ),
    );
  }
}

class _SharedItemTile extends StatefulWidget {
  final GetChatResponseObject item;
  final _SharedItemsLayout layout;

  const _SharedItemTile({required this.item, required this.layout});

  @override
  State<_SharedItemTile> createState() => _SharedItemTileState();
}

class _SharedItemTileState extends State<_SharedItemTile>
    with DownloadTrigger<_SharedItemTile> {
  RichObjectString get _file => widget.item.messageParameters!['file']!;

  @override
  String? get downloadRemotePath => _file.path;

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

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

  void _onTap() {
    if (guardDemoAction(context)) return;
    if (isDownloading) {
      confirmCancelDownload();
    } else {
      startDownload(name: _file.name, remoteFile: RemoteFileRef.fromTalk(_file));
    }
  }

  String get _previewUrl =>
      'https://${EndpointData().nextcloud().full()}'
      '/index.php/core/preview?fileId=${_file.id}&x=300&y=300&a=1';

  bool get _isVideo => _isVideoFile(_file.name);

  bool get _isDownloading => downloadJob?.status.value is DownloadInProgress;

  double? get _downloadProgress {
    final status = downloadJob?.status.value;
    if (status is! DownloadInProgress) return null;
    return status.percent <= 0 ? null : status.percent / 100;
  }

  @override
  Widget build(BuildContext context) => widget.layout == _SharedItemsLayout.grid
      ? _buildGrid(context)
      : _buildList(context);

  Widget _buildGrid(BuildContext context) => GestureDetector(
    onTap: _onTap,
    child: ColoredBox(
      color: Theme.of(context).colorScheme.surfaceContainerHighest,
      child: Stack(
        fit: StackFit.expand,
        children: [
          CachedNetworkImage(
            imageUrl: _previewUrl,
            httpHeaders: AccountData().authHeaders(),
            fit: BoxFit.cover,
            fadeInDuration: Duration.zero,
            fadeOutDuration: Duration.zero,
            errorListener: (_) {},
            placeholder: (context, url) =>
                const Center(child: AppProgressIndicator.small()),
            // Video thumbnails only exist when the server's preview provider
            // (ffmpeg) generated one; fall back to a film icon rather than a
            // broken-image glyph when it did not.
            errorWidget: (context, url, error) => Center(
              child: Icon(
                _isVideo
                    ? Icons.movie_outlined
                    : Icons.image_not_supported_outlined,
              ),
            ),
          ),
          if (_isVideo)
            const Center(
              child: DecoratedBox(
                decoration: BoxDecoration(
                  color: Colors.black45,
                  shape: BoxShape.circle,
                ),
                child: Padding(
                  padding: EdgeInsets.all(6),
                  child: Icon(Icons.play_arrow, color: Colors.white, size: 22),
                ),
              ),
            ),
          if (_isDownloading)
            const ColoredBox(
              color: Colors.black38,
              child: Center(
                child: AppProgressIndicator.small(color: Colors.white),
              ),
            ),
        ],
      ),
    ),
  );

  Widget _buildList(BuildContext context) => ListTile(
    leading: Icon(_iconForFile(_file.name), size: 36),
    title: Text(_file.name, maxLines: 2, overflow: TextOverflow.ellipsis),
    subtitle: Text(
      '${widget.item.actorDisplayName} · '
      '${DateTime.fromMillisecondsSinceEpoch(widget.item.timestamp * 1000).formatDateShort()}',
    ),
    trailing: _isDownloading
        ? SizedBox(
            width: 24,
            height: 24,
            child: CircularProgressIndicator(
              strokeWidth: 2,
              value: _downloadProgress,
            ),
          )
        : null,
    onTap: _onTap,
  );
}

bool _isVideoFile(String name) {
  final ext = name.contains('.') ? name.split('.').last.toLowerCase() : '';
  const videoExtensions = {
    'mp4',
    'mov',
    'mkv',
    'webm',
    'avi',
    'm4v',
    '3gp',
    'mpeg',
    'mpg',
    'wmv',
    'flv',
  };
  return videoExtensions.contains(ext);
}

IconData _iconForFile(String name) {
  if (_isVideoFile(name)) return Icons.movie_outlined;
  final ext = name.contains('.') ? name.split('.').last.toLowerCase() : '';
  switch (ext) {
    case 'pdf':
      return Icons.picture_as_pdf_outlined;
    case 'doc':
    case 'docx':
    case 'odt':
    case 'rtf':
    case 'txt':
      return Icons.description_outlined;
    case 'xls':
    case 'xlsx':
    case 'ods':
    case 'csv':
      return Icons.table_chart_outlined;
    case 'ppt':
    case 'pptx':
    case 'odp':
      return Icons.slideshow_outlined;
    case 'zip':
    case 'rar':
    case '7z':
    case 'tar':
    case 'gz':
      return Icons.folder_zip_outlined;
    case 'mp3':
    case 'wav':
    case 'm4a':
    case 'ogg':
      return Icons.audiotrack_outlined;
    default:
      return Icons.insert_drive_file_outlined;
  }
}

class _ErrorState extends StatelessWidget {
  final String message;
  final Future<void> Function() onRetry;

  const _ErrorState({required this.message, required this.onRetry});

  @override
  Widget build(BuildContext context) => Center(
    child: Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.error_outline, size: 40),
          const SizedBox(height: 12),
          Text(message, textAlign: TextAlign.center),
          const SizedBox(height: 12),
          FilledButton.tonal(
            onPressed: onRetry,
            child: const Text('Erneut versuchen'),
          ),
        ],
      ),
    ),
  );
}
