import 'package:dio/dio.dart';

import '../../../shared/utils/app_logger.dart';
import '../../contracts/trakt/trakt_response.dart';
import '../../contracts/trakt/trakt_sync_job.dart';
import '../../domain/ports/authed_trakt_gateway.dart';
import 'trakt_auth.dart';
import 'trakt_credentials.dart';
import 'trakt_rest_api.dart' show TraktHeaderInterceptor;

const _tag = 'AuthedTrakt';


class AuthedTraktClient implements AuthedTraktGateway {
  AuthedTraktClient({required this._auth, Dio? dio})
    : _dio =
          (dio ??
                Dio(
                  BaseOptions(
                    baseUrl: TraktCredentials.apiBaseUrl,
                    connectTimeout: const Duration(seconds: 10),
                    receiveTimeout: const Duration(seconds: 15),
                    headers: {'Accept': 'application/json'},
                  ),
                ))
            ..interceptors.add(TraktHeaderInterceptor());

  final TraktAuth _auth;
  final Dio _dio;

  @override
  Future<bool> isConnected() => _auth.isConnected();

  @override
  Future<TraktScrobbleOutcome> scrobble(
    TraktSyncJobType type,
    Map<String, dynamic> body,
  ) {
    return _post(_scrobblePath(type), body, allowRefreshRetry: true);
  }

  @override
  Future<TraktScrobbleOutcome> syncHistory(
    TraktSyncJobType type,
    Map<String, dynamic> body,
  ) {
    final path = type == TraktSyncJobType.historyRemove
        ? '/sync/history/remove'
        : '/sync/history';
    return _post(path, body, allowRefreshRetry: true);
  }

  @override
  Future<List<Map<String, dynamic>>> fetchPlayback() =>
      _getList('/sync/playback?extended=full');

  @override
  Future<List<Map<String, dynamic>>> fetchCalendarShows() =>
      _getList('/calendars/my/shows/${_todayUtc()}/7');

  @override
  Future<List<Map<String, dynamic>>> fetchCalendarMovies() =>
      _getList('/calendars/my/movies/${_todayUtc()}/7');

  static String _todayUtc() {
    final now = DateTime.now().toUtc();
    return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
  }


  Future<List<Map<String, dynamic>>> _getList(
    String path, {
    bool allowRefreshRetry = true,
  }) async {
    final token = await _auth.validAccessToken();
    if (token == null) throw StateError('Trakt not connected');

    try {
      final res = await _dio.get<dynamic>(
        path,
        options: Options(
          headers: {'Authorization': 'Bearer $token'},
          validateStatus: (_) => true,
        ),
      );
      final status = res.statusCode ?? 0;
      if (status == 401 && allowRefreshRetry) {
        final refreshed = await _auth.forceRefresh();
        if (refreshed == null) throw StateError('Trakt token refresh failed');
        return _getList(path, allowRefreshRetry: false);
      }
      if (status < 200 || status >= 300) {
        throw StateError('Trakt GET $path → $status');
      }
      final data = res.data;
      if (data is! List) return const [];
      return data.whereType<Map<String, dynamic>>().toList();
    } on DioException catch (e) {
      AppLogger.debug(_tag, 'GET $path network error', e);
      throw StateError('Trakt network error on GET $path: $e');
    }
  }

  static String _scrobblePath(TraktSyncJobType type) {
    switch (type) {
      case TraktSyncJobType.scrobbleStart:
        return '/scrobble/start';
      case TraktSyncJobType.scrobblePause:
        return '/scrobble/pause';
      case TraktSyncJobType.scrobbleStop:
      default:
        return '/scrobble/stop';
    }
  }

  Future<TraktScrobbleOutcome> _post(
    String path,
    Map<String, dynamic> body, {
    required bool allowRefreshRetry,
  }) async {
    final token = await _auth.validAccessToken();
    if (token == null) {
      return const TraktScrobbleOutcome(TraktResponseClass.unauthorized);
    }

    try {
      final res = await _dio.post<dynamic>(
        path,
        data: body,
        options: Options(
          headers: {'Authorization': 'Bearer $token'},
          validateStatus: (_) => true,
        ),
      );
      final status = res.statusCode ?? 0;
      final cls = classifyTraktStatus(status);

      if (cls == TraktResponseClass.success) {
        final data = res.data;
        AppLogger.debug(
          _tag,
          'scrobble $path → $status '
          'sent=${_identitySummary(body)} '
          'matched=${data is Map ? _identitySummary(data) : '?'} '
          'action=${data is Map ? data['action'] : '?'} '
          'trakt_progress=${data is Map ? data['progress'] : '?'}',
        );
      } else {
        AppLogger.debug(_tag, 'scrobble $path → $status (${cls.name})');
      }

      if (cls == TraktResponseClass.unauthorized && allowRefreshRetry) {
        final refreshed = await _auth.forceRefresh();
        if (refreshed != null) {
          return _post(path, body, allowRefreshRetry: false);
        }
        return const TraktScrobbleOutcome(TraktResponseClass.unauthorized);
      }

      if (cls == TraktResponseClass.rateLimited) {
        return TraktScrobbleOutcome(
          cls,
          retryAfter: _parseRetryAfter(res.headers.value('retry-after')),
        );
      }
      return TraktScrobbleOutcome(cls);
    } on DioException catch (e) {
      final status = e.response?.statusCode;
      if (status != null) {
        final cls = classifyTraktStatus(status);
        if (cls == TraktResponseClass.rateLimited) {
          return TraktScrobbleOutcome(
            cls,
            retryAfter: _parseRetryAfter(
              e.response?.headers.value('retry-after'),
            ),
          );
        }
        return TraktScrobbleOutcome(cls);
      }
      AppLogger.debug(_tag, 'network error on $path → transient', e);
      return const TraktScrobbleOutcome(TraktResponseClass.transient);
    } catch (e) {
      AppLogger.debug(_tag, 'unexpected error on $path → transient', e);
      return const TraktScrobbleOutcome(TraktResponseClass.transient);
    }
  }


  static String _identitySummary(Map<dynamic, dynamic> data) {
    final movie = data['movie'];
    if (movie is Map) return 'movie "${movie['title']}" ids=${movie['ids']}';
    final show = data['show'];
    final episode = data['episode'];
    if (show is Map || episode is Map) {
      final showTitle = show is Map ? show['title'] : null;
      final showIds = show is Map ? show['ids'] : null;
      final s = episode is Map ? episode['season'] : null;
      final n = episode is Map ? episode['number'] : null;
      final epIds = episode is Map ? episode['ids'] : null;
      return 'episode "$showTitle" S${s}E$n showIds=$showIds epIds=$epIds';
    }
    return 'none';
  }


  static Duration? _parseRetryAfter(String? raw) {
    if (raw == null || raw.isEmpty) return null;
    final secs = int.tryParse(raw.trim());
    if (secs == null || secs < 0) return null;
    return Duration(seconds: secs);
  }
}
