import 'dart:async';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
import 'package:nextcloud/nextcloud.dart';

import '../api_error.dart';
import '../http_errors.dart';
import '../marianumcloud/talk/talk_error.dart';
import 'app_exception.dart';
import 'auth_exception.dart';
import 'network_exception.dart';
import 'not_found_exception.dart';
import 'parse_exception.dart';
import 'server_exception.dart';
import 'talk_exception.dart';

const String _defaultFallback =
    'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
const String _tlsErrorMessage =
    'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). '
    'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).';

AppException? _dioToAppException(DioException error) {
  switch (error.type) {
    case DioExceptionType.connectionTimeout:
    case DioExceptionType.sendTimeout:
    case DioExceptionType.receiveTimeout:
    case DioExceptionType.transformTimeout:
      return NetworkException.timeout(technicalDetails: error.message);
    case DioExceptionType.connectionError:
      return NetworkException(technicalDetails: error.message);
    case DioExceptionType.badCertificate:
      return const NetworkException(userMessage: _tlsErrorMessage);
    case DioExceptionType.badResponse:
      final status = error.response?.statusCode;
      return ServerException(
        statusCode: status ?? -1,
        technicalDetails: 'HTTP $status: ${error.message}',
      );
    case DioExceptionType.cancel:
    case DioExceptionType.unknown:
      final inner = error.error;
      if (inner is SocketException) {
        return NetworkException(technicalDetails: inner.message);
      }
      if (inner is HandshakeException) {
        return const NetworkException(userMessage: _tlsErrorMessage);
      }
      if (inner is FormatException) {
        return ParseException(technicalDetails: inner.message);
      }
      return null;
  }
}

/// The nextcloud/dynamite clients throw [DynamiteApiException] on non-2xx.
/// Its toString() dumps every response header, so the details keep only the
/// status plus a trimmed body preview (same format as the Talk API errors).
AppException _dynamiteToAppException(DynamiteApiException error) {
  final status = error.statusCode;
  final preview = previewBody(error.body);
  final detail = preview.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
  switch (status) {
    case 401:
      return AuthException.unauthorized(technicalDetails: detail);
    case 403:
      return AuthException.forbidden(technicalDetails: detail);
    case 404:
      return NotFoundException(technicalDetails: detail);
    case 429:
      return ServerException(
        statusCode: status,
        userMessage:
            'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut versuchst.',
        technicalDetails: detail,
      );
    default:
      return ServerException(statusCode: status, technicalDetails: detail);
  }
}

String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
  if (error == null) return fallback;
  if (error is AppException) return error.userMessage;

  if (error is TalkError) return TalkException(error).userMessage;

  if (error is DioException) {
    final mapped = _dioToAppException(error);
    if (mapped != null) return mapped.userMessage;
  }

  if (error is DynamiteApiException) {
    return _dynamiteToAppException(error).userMessage;
  }

  if (error is SocketException) {
    return const NetworkException().userMessage;
  }
  if (error is TimeoutException) {
    return NetworkException.timeout().userMessage;
  }
  if (error is http.ClientException) {
    return const NetworkException().userMessage;
  }
  if (error is HandshakeException) {
    return _tlsErrorMessage;
  }
  if (error is FormatException) {
    return const ParseException().userMessage;
  }
  if (error is ApiError) {
    return _stripDioPrefix(error.message);
  }

  return fallback;
}

String? errorToTechnicalDetails(Object? error) {
  if (error == null) return null;
  if (error is AppException) return error.technicalDetails ?? error.toString();
  if (error is TalkError) return TalkException(error).technicalDetails;
  if (error is DioException) {
    final mapped = _dioToAppException(error);
    if (mapped != null) return mapped.technicalDetails ?? mapped.toString();
  }
  if (error is DynamiteApiException) {
    final mapped = _dynamiteToAppException(error);
    return mapped.technicalDetails ?? mapped.toString();
  }
  return error.toString();
}

bool errorAllowsRetry(Object? error) {
  if (error == null) return true;
  if (error is AppException) return error.allowRetry;
  if (error is DioException) {
    final mapped = _dioToAppException(error);
    if (mapped != null) return mapped.allowRetry;
  }
  if (error is DynamiteApiException) {
    return _dynamiteToAppException(error).allowRetry;
  }
  return true;
}

String _stripDioPrefix(String raw) {
  // ApiError messages embed full request URIs; only surface the first line.
  final firstLine = raw.split('\n').first.trim();
  return firstLine.isEmpty ? _defaultFallback : firstLine;
}
