import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'
    show HttpExceptionWithStatus;
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../auth/auth_provider.dart';

/// CachedNetworkImage wrapper that resolves server-relative URLs (e.g.
/// `/api/albums/<id>/cover`) against the configured server base URL
/// from [serverUrlProvider]. Absolute URLs (with scheme) pass through
/// unchanged. Empty/null base or empty URL render the [fallback]
/// (defaults to a transparent SizedBox so callers can supply their own
/// surrounding placeholder).
///
/// Mirrors the absolute-or-relative logic the audio handler uses for
/// stream URLs, so cover art and audio resolve URLs the same way.
///
/// Uses CachedNetworkImage so cover bytes land on disk (path_provider
/// temp dir, keyed by URL) and survive scroll-off + app restart. The
/// previous Image.network behavior cached only in-memory, so the cold-
/// start home grid re-downloaded every cover.
class ServerImage extends ConsumerWidget {
  const ServerImage({
    super.key,
    required this.url,
    this.fit,
    this.fallback,
  });

  final String url;
  final BoxFit? fit;
  final Widget? fallback;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final empty = fallback ?? const SizedBox.shrink();
    if (url.isEmpty) return empty;
    final base = ref.watch(serverUrlProvider).value;
    final resolved = _resolve(base, url);
    if (resolved == null) return empty;
    // Cover endpoints are gated by RequireUser server-side. The image
    // loader doesn't carry cookies/Bearer tokens automatically the way
    // the browser's <img> tag does, so we forward the session token as
    // a header. Without this, the server returns 401 and the image
    // fails.
    //
    // sessionTokenProvider is a FutureProvider — on first read after a
    // hot restart its .value is null until the secure-storage read
    // resolves. If we mounted the image with httpHeaders:null at that
    // moment, the disk cache would lock in the 401 response and never
    // retry. Hold the fallback until the token is available, then
    // mount the image with the auth header attached.
    final tokenAsync = ref.watch(sessionTokenProvider);
    return tokenAsync.when(
      loading: () => empty,
      error: (_, __) => empty,
      data: (token) {
        final headers = (token != null && token.isNotEmpty)
            ? {'Authorization': 'Bearer $token'}
            : <String, String>{};
        return CachedNetworkImage(
          imageUrl: resolved,
          httpHeaders: headers,
          fit: fit,
          // 120ms feels like cover bytes settling in on a cache miss
          // (smoother than the abrupt zero-fade); on cache hits the
          // image is decoded synchronously so the fade is imperceptible.
          // The default 500ms is too long — looks like a regression on
          // a populated grid.
          fadeInDuration: const Duration(milliseconds: 120),
          fadeOutDuration: Duration.zero,
          // Keep failures local — a single 401/timeout shouldn't dump
          // a stack trace or replace the parent Container's background.
          errorWidget: (_, __, ___) => empty,
          // Filter expected 404s out of dev console noise: playlist
          // collages aren't built until the playlist has tracks and
          // the build job runs (system playlists like For-You /
          // Discover hit this on first-render). The errorWidget
          // already renders the fallback for the user; this just
          // keeps the dev console clean. Non-404 errors still
          // surface so auth/connectivity issues remain visible.
          errorListener: (err) {
            if (err is HttpExceptionWithStatus && err.statusCode == 404) {
              return;
            }
            debugPrint('ServerImage: $err');
          },
        );
      },
    );
  }

  static String? _resolve(String? baseUrl, String url) {
    final parsed = Uri.tryParse(url);
    if (parsed != null && parsed.hasScheme) return url;
    if (baseUrl == null || baseUrl.isEmpty) return null;
    final base = baseUrl.endsWith('/')
        ? baseUrl.substring(0, baseUrl.length - 1)
        : baseUrl;
    final path = url.startsWith('/') ? url : '/$url';
    return '$base$path';
  }
}
