import 'dart:io';

import 'package:audio_service/audio_service.dart';
import 'package:cached_network_image/cached_network_image.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/endpoints/likes.dart' show LikeKind;
import '../likes/like_button.dart';
import '../models/track.dart';
import '../shared/widgets/track_actions/track_actions_button.dart';
import '../theme/theme_extension.dart';
import 'now_playing_screen.dart' show kPlayerCoverHeroTag;
import 'player_provider.dart';

/// Compact player bar mounted at the bottom of the app shell. Mini
/// view only — the heavyweight shuffle/repeat/queue/volume controls
/// live in NowPlayingScreen, accessible by tap or swipe-up.
///
///   ┌─────────────────────────────────────┬──────────┐
///   │ [art] Title          ♥ ⋮              │ ⏮ ⏯ ⏭   │
///   │       Artist                          │          │
///   ├─────────────────────────────────────────────────┤
///   │ 0:00 ━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3:06       │
///   └─────────────────────────────────────────────────┘
///
/// Tap anywhere on the bar (including art/title) ascends to the full
/// player. A vertical drag upward also ascends, so the gesture mirrors
/// the drag-down dismissal on the full screen.
class PlayerBar extends ConsumerStatefulWidget {
  const PlayerBar({super.key});

  @override
  ConsumerState<PlayerBar> createState() => _PlayerBarState();
}

class _PlayerBarState extends ConsumerState<PlayerBar> {
  /// Last non-null artUri we've seen from the mediaItem stream. Held
  /// across the audio_handler's two-broadcast track-change pattern
  /// (bare MediaItem first, then with artUri once AlbumCoverCache
  /// resolves) so the mini bar keeps showing the previous track's
  /// cover until the new one is known. Without this hold the bar
  /// flickered to the slate placeholder for ~100ms on every track
  /// change.
  Uri? _lastArtUri;

  @override
  Widget build(BuildContext context) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    final media = ref.watch(mediaItemProvider).value;
    final playback = ref.watch(playbackStateProvider).value;
    if (media == null) return const SizedBox.shrink();

    // Capture the latest non-null artUri so _TrackInfo's cover stays
    // continuous across track changes.
    if (media.artUri != null) _lastArtUri = media.artUri;
    final displayMedia = media.artUri == null && _lastArtUri != null
        ? media.copyWith(artUri: _lastArtUri)
        : media;

    // positionProvider is just_audio's positionStream (~200ms cadence)
    // so the mini bar's seek crawls forward smoothly. PlaybackState
    // only updates on event transitions and would leave it frozen.
    final pos = ref.watch(positionProvider).value ?? Duration.zero;
    final dur = media.duration ?? Duration.zero;

    return Material(
      color: fs.iron,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () => context.push('/now-playing'),
        // Swipe up anywhere on the bar to expand into the full player.
        // We only act on a clear upward flick (>200 px/s) so a slow
        // tap-with-tiny-jitter doesn't accidentally open the screen.
        onVerticalDragEnd: (d) {
          final v = d.primaryVelocity ?? 0;
          if (v < -200) context.push('/now-playing');
        },
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              IntrinsicHeight(
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    Expanded(child: _TrackInfo(media: displayMedia)),
                    const SizedBox(width: 8),
                    _PlayControls(playback: playback, ref: ref),
                  ],
                ),
              ),
              const SizedBox(height: 4),
              _SeekRow(position: pos, duration: dur),
            ],
          ),
        ),
      ),
    );
  }
}

class _TrackInfo extends StatelessWidget {
  const _TrackInfo({required this.media});
  final MediaItem media;

  @override
  Widget build(BuildContext context) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    final artistName = (media.artist ?? '').trim();

    // Cover is wrapped in a Hero with the shared kPlayerCoverHeroTag so
    // tapping the mini bar to expand into NowPlayingScreen animates the
    // artwork from this 48dp footprint to the full-screen size rather
    // than fade-cutting. Tag is stable per-route (not keyed by media.id)
    // so the transition works regardless of what's playing.
    //
    // Why no transition / placeholder handling: the audio_handler
    // broadcasts MediaItem twice on track change — once with artUri
    // null (the new track's bare metadata), then with artUri set once
    // AlbumCoverCache resolves. The widget tree above hands us the
    // most-recent non-null artUri (`displayArtUri`), so the previous
    // track's cover stays visible across that null gap and the new
    // cover snaps in the moment its artUri arrives. Rapid change is
    // fine here per operator preference; AnimatedSwitcher previously
    // smeared the swap and made the slate placeholder visible.
    final displayArtUri = media.artUri;
    final Widget cover;
    if (displayArtUri != null) {
      // CachedNetworkImageProvider for HTTPS art URIs so the mini bar
      // hits the same disk cache the rest of the UI uses (ServerImage,
      // discover thumbnails). FileImage stays for the file:// branch
      // populated by AlbumCoverCache — bytes are already on disk and a
      // second cache layer would only burn duplicate space.
      cover = Image(
        image: displayArtUri.isScheme('file')
            ? FileImage(File.fromUri(displayArtUri)) as ImageProvider
            : CachedNetworkImageProvider(displayArtUri.toString()),
        width: 48,
        height: 48,
        // Without a fit, Image paints the source at its intrinsic
        // resolution inside the 48dp box — a thumbnail-sized cover
        // would render as a tiny inset. Cover stretches/crops to fill
        // the box uniformly.
        fit: BoxFit.cover,
        errorBuilder: (_, __, ___) =>
            Container(width: 48, height: 48, color: fs.slate),
      );
    } else {
      cover = Container(width: 48, height: 48, color: fs.slate);
    }
    return Row(
      // Default centering vertically aligns the like/kebab buttons
      // against the album art (48dp) — visually they span the title +
      // artist block instead of sitting on the title baseline.
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Hero(
          tag: kPlayerCoverHeroTag,
          child: cover,
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                media.title,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(color: fs.parchment, fontSize: 14),
              ),
              if (artistName.isNotEmpty)
                Text(
                  artistName,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: TextStyle(color: fs.ash, fontSize: 12),
                ),
            ],
          ),
        ),
        // Like + kebab as siblings to the title/artist column rather
        // than nested inside the title row. Width stays 32dp each so
        // horizontal footprint matches what we had; height stretches
        // to the row's full 48dp so the icons sit at vertical center
        // against the title+artist block.
        SizedBox(
          width: 32,
          height: 48,
          child: LikeButton(
            kind: LikeKind.track,
            id: media.id,
            size: 20,
          ),
        ),
        SizedBox(
          width: 32,
          height: 48,
          child: TrackActionsButton(
            track: _trackRefFromMediaItem(media),
            hideQueueActions: true,
          ),
        ),
      ],
    );
  }
}

class _PlayControls extends StatelessWidget {
  const _PlayControls({required this.playback, required this.ref});
  final PlaybackState? playback;
  final WidgetRef ref;

  @override
  Widget build(BuildContext context) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    final isPlaying = playback?.playing == true;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        SizedBox(
          width: 36,
          height: 36,
          child: IconButton(
            padding: EdgeInsets.zero,
            iconSize: 22,
            icon: Icon(LucideIcons.skip_back, color: fs.parchment),
            onPressed: () => ref.read(audioHandlerProvider).skipToPrevious(),
          ),
        ),
        SizedBox(
          width: 44,
          height: 44,
          child: IconButton(
            padding: EdgeInsets.zero,
            iconSize: 32,
            icon: Icon(
              isPlaying ? LucideIcons.circle_pause : LucideIcons.circle_play,
              color: fs.accent,
            ),
            onPressed: () {
              final h = ref.read(audioHandlerProvider);
              if (isPlaying) {
                h.pause();
              } else {
                h.play();
              }
            },
          ),
        ),
        SizedBox(
          width: 36,
          height: 36,
          child: IconButton(
            padding: EdgeInsets.zero,
            iconSize: 22,
            icon: Icon(LucideIcons.skip_forward, color: fs.parchment),
            onPressed: () => ref.read(audioHandlerProvider).skipToNext(),
          ),
        ),
      ],
    );
  }
}

class _SeekRow extends ConsumerWidget {
  const _SeekRow({required this.position, required this.duration});
  final Duration position;
  final Duration duration;

  String _fmt(Duration d) {
    final m = d.inMinutes.remainder(60).toString();
    final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$m:$s';
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    final maxMs =
        duration.inMilliseconds.toDouble().clamp(1.0, double.infinity);
    return Row(
      children: [
        SizedBox(
          width: 36,
          child: Text(
            _fmt(position),
            textAlign: TextAlign.right,
            style: TextStyle(color: fs.ash, fontSize: 10),
          ),
        ),
        Expanded(
          child: SliderTheme(
            data: SliderTheme.of(context).copyWith(
              trackHeight: 2,
              thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
              overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
            ),
            child: Slider(
              activeColor: fs.accent,
              inactiveColor: fs.slate,
              min: 0,
              max: maxMs,
              value: position.inMilliseconds
                  .toDouble()
                  .clamp(0.0, duration.inMilliseconds.toDouble()),
              onChanged: (v) => ref
                  .read(audioHandlerProvider)
                  .seek(Duration(milliseconds: v.toInt())),
            ),
          ),
        ),
        SizedBox(
          width: 36,
          child: Text(
            _fmt(duration),
            style: TextStyle(color: fs.ash, fontSize: 10),
          ),
        ),
      ],
    );
  }
}

/// Reconstructs a minimal TrackRef from a MediaItem so the
/// TrackActionsButton has the fields its sheet expects. Mirrors the
/// pattern used by NowPlayingScreen — extras['album_id'] is what
/// audio_handler stashed at queue-build time.
TrackRef _trackRefFromMediaItem(MediaItem media) => TrackRef(
      id: media.id,
      title: media.title,
      albumId: (media.extras?['album_id'] as String?) ?? '',
      albumTitle: media.album ?? '',
      // artist_id is stashed in extras by audio_handler — without it
      // the kebab's "Go to artist" pushes /artists/ (empty id) and 404s.
      artistId: (media.extras?['artist_id'] as String?) ?? '',
      artistName: media.artist ?? '',
      durationSec: media.duration?.inSeconds ?? 0,
      streamUrl: '',
    );
