import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import '../api/errors.dart';
import '../cache/offline_provider.dart';
import '../cache/shuffle_source.dart';
import '../cache/tile_providers.dart';
import '../models/playlist.dart';
import '../models/system_playlists_status.dart';
import '../models/track.dart';
import '../player/player_provider.dart';
import '../playlists/playlists_provider.dart';
import '../playlists/widgets/playlist_card.dart';
import '../playlists/widgets/playlist_placeholder_card.dart';
import '../shared/widgets/connection_error_banner.dart';
import '../shared/widgets/main_app_bar_actions.dart';
import '../shared/widgets/skeletons.dart';
import '../theme/theme_extension.dart';
import 'library_providers.dart';
import 'widgets/album_card.dart';
import 'widgets/artist_card.dart';
import 'widgets/compact_track_card.dart';
import 'widgets/horizontal_scroll_row.dart';

/// Home screen. Per-item rendering: a tiny /api/home/index discovery
/// fetch returns just section→IDs, then each tile hydrates itself
/// against the per-entity drift tables (sync-populated) with REST
/// fallback via the HydrationQueue. Cold-visit dead air shrinks to a
/// single small round-trip; tiles materialize as their data lands.
class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    final index = ref.watch(homeIndexProvider);
    final allPlaylists = ref.watch(playlistsListProvider('all'));
    final status = ref.watch(systemPlaylistsStatusProvider);
    return Scaffold(
      backgroundColor: fs.obsidian,
      appBar: AppBar(
        backgroundColor: fs.obsidian,
        elevation: 0,
        title: Text('Minstrel', style: TextStyle(color: fs.parchment)),
        actions: const [MainAppBarActions(currentRoute: '/home')],
      ),
      body: SafeArea(
        child: index.when(
          error: (e, _) {
            final code = e is DioException ? ApiError.fromDio(e).code : 'unknown';
            if (code == 'connection_refused') {
              return ConnectionErrorBanner(
                onRetry: () => ref.refresh(homeIndexProvider),
              );
            }
            return Center(child: Text('$e', style: TextStyle(color: fs.error)));
          },
          loading: () => _HomeSkeleton(fs: fs),
          data: (h) => RefreshIndicator(
            onRefresh: () async => ref.refresh(homeIndexProvider.future),
            child: ListView(
              physics: const ClampingScrollPhysics(),
              children: [
                _PlaylistsSection(
                  playlists: allPlaylists.value?.owned ?? const [],
                  status: status.value ?? SystemPlaylistsStatus.empty(),
                  offline: ref.watch(offlineProvider),
                ),
                _RecentlyAddedSection(ids: h.recentlyAddedAlbums),
                _RediscoverSection(
                  albumIds: h.rediscoverAlbums,
                  artistIds: h.rediscoverArtists,
                ),
                _MostPlayedSection(ids: h.mostPlayedTracks),
                _LastPlayedSection(ids: h.lastPlayedArtists),
                const SizedBox(height: 140),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// ─── Per-tile widgets ────────────────────────────────────────────────

/// Duration of the skeleton→content cross-fade. 220ms reads as "tile
/// settled into place" — longer drags, shorter feels like a hard cut.
/// Each tile cross-fades independently when its data lands, so the
/// natural cascade from the hydration queue's bounded concurrency
/// produces a staged-reveal feel without any per-tile delay math.
const Duration _tileRevealDuration = Duration(milliseconds: 220);

/// Album tile: skeleton until albumTileProvider yields a populated row.
class _AlbumTile extends ConsumerWidget {
  const _AlbumTile({required this.id});
  final String id;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncAlbum = ref.watch(albumTileProvider(id));
    final album = asyncAlbum.asData?.value;
    return AnimatedSwitcher(
      duration: _tileRevealDuration,
      switchInCurve: Curves.easeOut,
      child: album == null
          ? const SkeletonAlbumTile(key: ValueKey('skeleton'))
          : AlbumCard(
              key: ValueKey('album-${album.id}'),
              album: album,
              onTap: () =>
                  context.push('/albums/${album.id}', extra: album),
            ),
    );
  }
}

/// Artist tile: skeleton until artistTileProvider yields a populated row.
class _ArtistTile extends ConsumerWidget {
  const _ArtistTile({required this.id});
  final String id;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncArtist = ref.watch(artistTileProvider(id));
    final artist = asyncArtist.asData?.value;
    return AnimatedSwitcher(
      duration: _tileRevealDuration,
      switchInCurve: Curves.easeOut,
      child: artist == null
          ? const SkeletonArtistTile(key: ValueKey('skeleton'))
          : ArtistCard(
              key: ValueKey('artist-${artist.id}'),
              artist: artist,
              onTap: () =>
                  context.push('/artists/${artist.id}', extra: artist),
            ),
    );
  }
}

/// Track tile (used by Most-Played). On tap, gathers the currently-
/// hydrated TrackRefs for the whole section so playback flows like
/// it did with the bulk-loaded list. Tracks that haven't hydrated yet
/// are skipped from the play list — typically transient on a cold
/// visit since hydration runs in parallel and finishes quickly.
class _TrackTile extends ConsumerWidget {
  const _TrackTile({required this.id, required this.sectionIds});
  final String id;
  final List<String> sectionIds;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncTrack = ref.watch(trackTileProvider(id));
    final track = asyncTrack.asData?.value;
    return AnimatedSwitcher(
      duration: _tileRevealDuration,
      switchInCurve: Curves.easeOut,
      child: track == null
          ? const _CompactTrackSkeleton(key: ValueKey('skeleton'))
          : CompactTrackCard(
              key: ValueKey('track-${track.id}'),
              track: track,
              sectionTracks: _resolveSectionTracks(ref, sectionIds),
              index:
                  sectionIds.indexOf(id).clamp(0, sectionIds.length - 1),
            ),
    );
  }

  /// Snapshots the section's current track states. Used at tile
  /// construction time — CompactTrackCard's onTap uses it as the play
  /// queue. Tracks still hydrating are dropped; once they land, a
  /// rebuild re-runs this lookup so the queue grows naturally.
  static List<TrackRef> _resolveSectionTracks(WidgetRef ref, List<String> ids) {
    final out = <TrackRef>[];
    for (final i in ids) {
      final v = ref.read(trackTileProvider(i)).asData?.value;
      if (v != null) out.add(v);
    }
    return out;
  }
}

/// Compact-track placeholder. 56dp cover + two text lines, matched to
/// CompactTrackCard so swapping in the real card doesn't shift the
/// row's height.
class _CompactTrackSkeleton extends StatelessWidget {
  const _CompactTrackSkeleton({super.key});
  @override
  Widget build(BuildContext context) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    return Container(
      width: 240,
      margin: const EdgeInsets.symmetric(horizontal: 4),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
      child: Row(children: [
        Container(
          width: 56,
          height: 56,
          decoration: BoxDecoration(
            color: fs.slate,
            borderRadius: BorderRadius.circular(6),
          ),
        ),
        const SizedBox(width: 8),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(width: 120, height: 12, color: fs.slate),
              const SizedBox(height: 6),
              Container(width: 80, height: 10, color: fs.slate),
            ],
          ),
        ),
      ]),
    );
  }
}

// ─── Sections ────────────────────────────────────────────────────────

class _PlaylistsSection extends StatelessWidget {
  const _PlaylistsSection({
    required this.playlists,
    required this.status,
    required this.offline,
  });
  final List<Playlist> playlists;
  final SystemPlaylistsStatus status;
  final bool offline;

  @override
  Widget build(BuildContext context) {
    final items = _buildPlaylistsRow(playlists, status);
    final children = <Widget>[
      // Offline: surface the cache-backed pools where the (now
      // play-disabled) system playlists sit, so there's something
      // to play. #427 S4b.
      if (offline) ...const [
        _OfflinePoolCard(
          label: 'Recently played',
          icon: LucideIcons.history,
          kind: _OfflinePoolKind.recentlyPlayed,
        ),
        _OfflinePoolCard(
          label: 'Liked',
          icon: LucideIcons.heart,
          kind: _OfflinePoolKind.liked,
        ),
      ],
      for (final item in items)
        if (item is _RealPlaylist)
          PlaylistCard(playlist: item.playlist)
        else
          PlaylistPlaceholderCard(
            label: (item as _PlaceholderPlaylist).label,
            variant: item.variant,
          ),
    ];
    return HorizontalScrollRow(
      title: 'Playlists',
      height: 220,
      children: children,
    );
  }
}

enum _OfflinePoolKind { recentlyPlayed, liked }

/// Home tile for an offline cache-backed pool. Tapping shuffles +
/// plays that pool from the local cache. Sized to match
/// PlaylistCard so the row stays visually consistent.
class _OfflinePoolCard extends ConsumerWidget {
  const _OfflinePoolCard({
    required this.label,
    required this.icon,
    required this.kind,
  });
  final String label;
  final IconData icon;
  final _OfflinePoolKind kind;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    return SizedBox(
      width: 176,
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () async {
            final messenger = ScaffoldMessenger.of(context);
            final src = ref.read(shuffleSourceProvider);
            final refs = await switch (kind) {
              _OfflinePoolKind.recentlyPlayed => src.recentlyPlayed(),
              _OfflinePoolKind.liked => src.liked(),
            };
            if (refs.isEmpty) {
              messenger.showSnackBar(
                SnackBar(content: Text('No cached $label tracks yet')),
              );
              return;
            }
            await ref
                .read(playerActionsProvider)
                .playTracks(refs, shuffle: true);
          },
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  width: 144,
                  height: 144,
                  decoration: BoxDecoration(
                    color: fs.slate,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: Icon(icon, color: fs.accent, size: 56),
                ),
                const SizedBox(height: 8),
                Text(
                  label,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: TextStyle(color: fs.parchment, fontSize: 14),
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 2),
                  child: Text(
                    'Offline',
                    style: TextStyle(color: fs.ash, fontSize: 11),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

abstract class _PlaylistRowItem {}
class _RealPlaylist extends _PlaylistRowItem {
  _RealPlaylist(this.playlist);
  final Playlist playlist;
}
class _PlaceholderPlaylist extends _PlaylistRowItem {
  _PlaceholderPlaylist(this.label, this.variant);
  final String label;
  final String variant;
}

List<_PlaylistRowItem> _buildPlaylistsRow(
  List<Playlist> ownedAll,
  SystemPlaylistsStatus status,
) {
  final out = <_PlaylistRowItem>[];

  Playlist? findFirst(bool Function(Playlist) test) {
    for (final p in ownedAll) {
      if (test(p)) return p;
    }
    return null;
  }

  final forYou = findFirst((p) => p.systemVariant == 'for_you');
  out.add(forYou != null
      ? _RealPlaylist(forYou)
      : _PlaceholderPlaylist('For You', _variantFor('for-you', status)));

  final discover = findFirst((p) => p.systemVariant == 'discover');
  out.add(discover != null
      ? _RealPlaylist(discover)
      : _PlaceholderPlaylist('Discover', _variantFor('discover', status)));

  final songsLike = ownedAll
      .where((p) => p.systemVariant == 'songs_like_artist')
      .take(3)
      .toList();
  for (var i = 0; i < 3; i++) {
    out.add(i < songsLike.length
        ? _RealPlaylist(songsLike[i])
        : _PlaceholderPlaylist('Songs like…', _variantFor('songs-like', status)));
  }

  for (final p in ownedAll.where((p) => p.systemVariant == null)) {
    out.add(_RealPlaylist(p));
  }

  return out;
}

String _variantFor(String slot, SystemPlaylistsStatus s) {
  if (s.inFlight) return 'building';
  if (s.lastError != null) return 'failed';
  if (slot == 'songs-like' && s.lastRunAt != null) return 'seed-needed';
  return 'pending';
}

class _RecentlyAddedSection extends StatelessWidget {
  const _RecentlyAddedSection({required this.ids});
  final List<String> ids;

  @override
  Widget build(BuildContext context) {
    if (ids.isEmpty) {
      return const _EmptySection(
        title: 'Recently added',
        message: "Nothing added yet. Scan a folder via the server's config.",
      );
    }
    final controller = ScrollController();
    final rows = _chunk(ids, 25);
    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      for (var i = 0; i < rows.length; i++)
        HorizontalScrollRow(
          title: i == 0 ? 'Recently added' : '',
          controller: controller,
          children: rows[i].map((id) => _AlbumTile(id: id)).toList(),
        ),
    ]);
  }
}

class _RediscoverSection extends StatelessWidget {
  const _RediscoverSection({required this.albumIds, required this.artistIds});
  final List<String> albumIds;
  final List<String> artistIds;

  @override
  Widget build(BuildContext context) {
    if (albumIds.isEmpty && artistIds.isEmpty) {
      return const _EmptySection(
        title: 'Rediscover',
        message:
            'No forgotten favourites yet. Like some albums or artists to fill this in.',
      );
    }
    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      if (albumIds.isNotEmpty)
        HorizontalScrollRow(
          title: 'Rediscover',
          children: albumIds.map((id) => _AlbumTile(id: id)).toList(),
        ),
      if (artistIds.isNotEmpty)
        HorizontalScrollRow(
          title: albumIds.isEmpty ? 'Rediscover' : '',
          height: 168,
          children: artistIds.map((id) => _ArtistTile(id: id)).toList(),
        ),
    ]);
  }
}

class _MostPlayedSection extends StatelessWidget {
  const _MostPlayedSection({required this.ids});
  final List<String> ids;

  @override
  Widget build(BuildContext context) {
    if (ids.isEmpty) {
      return const _EmptySection(
        title: 'Most played',
        message: 'No plays to draw from. Listen to something.',
      );
    }
    final controller = ScrollController();
    final rows = _chunk(ids, 25);
    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      for (var i = 0; i < rows.length; i++)
        HorizontalScrollRow(
          title: i == 0 ? 'Most played' : '',
          height: 64,
          controller: controller,
          children: [
            for (final id in rows[i]) _TrackTile(id: id, sectionIds: ids),
          ],
        ),
    ]);
  }
}

class _LastPlayedSection extends StatelessWidget {
  const _LastPlayedSection({required this.ids});
  final List<String> ids;

  @override
  Widget build(BuildContext context) {
    if (ids.isEmpty) {
      return const _EmptySection(
        title: 'Last played',
        message: 'No recent plays.',
      );
    }
    return HorizontalScrollRow(
      title: 'Last played',
      height: 168,
      children: ids.map((id) => _ArtistTile(id: id)).toList(),
    );
  }
}

class _EmptySection extends StatelessWidget {
  const _EmptySection({required this.title, required this.message});
  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Text(
          title,
          style: TextStyle(
            fontFamily: fs.display.fontFamily,
            fontSize: 18,
            color: fs.parchment,
          ),
        ),
        const SizedBox(height: 8),
        Text(message, style: TextStyle(color: fs.ash)),
      ]),
    );
  }
}

/// Cold-start skeleton — shown only between mount and the first
/// homeIndexProvider emission (typically a single drift query + small
/// REST round-trip). Each section then renders its own per-tile
/// skeletons internally, so this widget's role is just the very first
/// pre-discovery frame.
class _HomeSkeleton extends StatelessWidget {
  const _HomeSkeleton({required this.fs});
  final FabledSwordTheme fs;

  @override
  Widget build(BuildContext context) {
    return ListView(
      physics: const ClampingScrollPhysics(),
      children: [
        _SkeletonSection(fs: fs, title: 'Playlists', cardWidth: 176),
        _SkeletonSection(fs: fs, title: 'Recently added', cardWidth: 140),
        _SkeletonSection(fs: fs, title: 'Rediscover', cardWidth: 140),
        _SkeletonSection(fs: fs, title: 'Most played', cardWidth: 140),
        _SkeletonSection(fs: fs, title: 'Last played', cardWidth: 140),
        const SizedBox(height: 140),
      ],
    );
  }
}

class _SkeletonSection extends StatelessWidget {
  const _SkeletonSection({
    required this.fs,
    required this.title,
    required this.cardWidth,
  });
  final FabledSwordTheme fs;
  final String title;
  final double cardWidth;

  @override
  Widget build(BuildContext context) {
    final coverSize = cardWidth - 16;
    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      Padding(
        padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
        child: Text(
          title,
          style: TextStyle(
            fontFamily: fs.display.fontFamily,
            fontSize: 18,
            color: fs.parchment,
          ),
        ),
      ),
      SizedBox(
        height: coverSize + 40,
        child: ListView.builder(
          scrollDirection: Axis.horizontal,
          padding: const EdgeInsets.symmetric(horizontal: 16),
          physics: const NeverScrollableScrollPhysics(),
          itemCount: 6,
          itemBuilder: (_, __) => SkeletonAlbumTile(width: cardWidth),
        ),
      ),
    ]);
  }
}

List<List<T>> _chunk<T>(List<T> items, int size) {
  final out = <List<T>>[];
  for (var i = 0; i < items.length; i += size) {
    out.add(items.sublist(i, i + size > items.length ? items.length : i + size));
  }
  return out;
}
