import 'dart:convert';
import 'dart:typed_data';

import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';

import '../friendica_client/paged_response.dart';
import '../globals.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/auth/profile.dart';
import '../models/circle_data.dart';
import '../models/connection.dart';
import '../models/direct_message.dart';
import '../models/exec_error.dart';
import '../models/follow_request.dart';
import '../models/gallery_data.dart';
import '../models/image_entry.dart';
import '../models/instance_info.dart';
import '../models/media_attachment_uploads/image_types_enum.dart';
import '../models/search_results.dart';
import '../models/search_types.dart';
import '../models/timeline_entry.dart';
import '../models/user_notification.dart';
import '../models/visibility.dart';
import '../serializers/friendica/direct_message_friendica_extensions.dart';
import '../serializers/friendica/gallery_data_friendica_extensions.dart';
import '../serializers/friendica/image_entry_friendica_extensions.dart';
import '../serializers/friendica/visibility_friendica_extensions.dart';
import '../serializers/mastodon/circle_data_mastodon_extensions.dart';
import '../serializers/mastodon/connection_mastodon_extensions.dart';
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
import '../serializers/mastodon/notification_mastodon_extension.dart';
import '../serializers/mastodon/search_result_mastodon_extensions.dart';
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
import '../services/fediverse_server_validator.dart';
import '../services/network_status_service.dart';
import '../utils/network_utils.dart';
import 'paging_data.dart';

class DirectMessagingClient extends FriendicaClient {
  static final _logger = Logger('$DirectMessagingClient');

  DirectMessagingClient(super.credentials) : super();

  FutureResult<List<DirectMessage>, ExecError> getDirectMessages(
      PagingData page) async {
    _networkStatusService.startDirectMessageUpdateStatus();
    final baseUrl = 'https://$serverName/api/direct_messages/all';
    final pagingQP = page.toQueryParameters(limitKeyword: 'count');
    final url = '$baseUrl?$pagingQP';
    final request = Uri.parse(url);
    _logger.finest(() => 'Getting direct messages with paging data $page');
    final result = (await _getApiListRequest(request).andThenSuccessAsync(
            (response) async => response.data
                .map((json) => DirectMessageFriendicaExtension.fromJson(json))
                .toList()))
        .execErrorCast();

    _networkStatusService.finishDirectMessageUpdateStatus();
    return result;
  }

  FutureResult<DirectMessage, ExecError> markDirectMessageRead(
      DirectMessage message) async {
    _networkStatusService.startDirectMessageUpdateStatus();
    final id = message.id;
    final url = Uri.parse(
        'https://$serverName/api/friendica/direct_messages_setseen?id=$id');
    final result = await postUrl(url, {}, headers: _headers)
        .andThenSuccessAsync((jsonString) async {
      return message.copy(seen: true);
    });
    _networkStatusService.finishDirectMessageUpdateStatus();
    return result.execErrorCast();
  }

  FutureResult<DirectMessage, ExecError> postDirectMessage(
    String? messageIdRepliedTo,
    String receivingUserId,
    String text,
  ) async {
    _networkStatusService.startDirectMessageUpdateStatus();
    final url = Uri.parse('https://$serverName/api/direct_messages/new');
    final body = {
      'user_id': receivingUserId,
      'text': text,
      if (messageIdRepliedTo != null) 'replyto': messageIdRepliedTo,
    };
    final result = await postUrl(url, body, headers: _headers)
        .andThenAsync<DirectMessage, ExecError>((jsonString) async {
      final json = jsonDecode(jsonString) as Map<String, dynamic>;
      if (json.containsKey('error')) {
        return buildErrorResult(
            type: ErrorType.serverError,
            message: "Error from server: ${json['error']}");
      }
      return Result.ok(
          DirectMessageFriendicaExtension.fromJson(jsonDecode(jsonString)));
    });

    _networkStatusService.finishDirectMessageUpdateStatus();
    return result.execErrorCast();
  }
}

class GalleryClient extends FriendicaClient {
  static final _logger = Logger('$GalleryClient');

  GalleryClient(super.credentials) : super();

  // TODO Convert Albums to using paging for real
  FutureResult<List<GalleryData>, ExecError> getGalleryData() async {
    _networkStatusService.startGalleryLoading();
    _logger.finest(() => 'Getting gallery data');
    final url = 'https://$serverName/api/friendica/photoalbums';
    final request = Uri.parse(url);
    final result = (await _getApiListRequest(request).andThenSuccessAsync(
            (albumsJson) async => albumsJson.data
                .map((json) => GalleryDataFriendicaExtensions.fromJson(json))
                .toList()))
        .execErrorCast();
    _networkStatusService.finishGalleryLoading();
    return result;
  }

  // TODO Convert Gallery Images to using paging for real once server side available
  FutureResult<List<ImageEntry>, ExecError> getGalleryImages(
    String galleryName,
    PagingData page,
  ) async {
    _networkStatusService.startGalleryLoading();
    _logger.finest(() => 'Getting gallery $galleryName data with page: $page');
    final baseUrl = 'https://$serverName/api/friendica/photoalbum?';
    final gallery = 'album=$galleryName&latest_first=true';
    final pageParams = page.toQueryParameters();
    final url = '$baseUrl$gallery&$pageParams';
    final request = Uri.parse(url);
    final result = (await _getApiListRequest(request).andThenSuccessAsync(
            (imagesJson) async => imagesJson.data
                .map((json) => ImageEntryFriendicaExtension.fromJson(json))
                .toList()))
        .execErrorCast();
    _networkStatusService.finishGalleryLoading();
    return result;
  }

  FutureResult<bool, ExecError> renameGallery(
      String oldGalleryName, String newGalleryName) async {
    _networkStatusService.startGalleryLoading();
    _logger.finest(() => 'Getting gallery data');
    final url =
        Uri.parse('https://$serverName/api/friendica/photoalbum/update');
    final body = {
      'album': oldGalleryName,
      'album_new': newGalleryName,
    };
    final result = await postUrl(
      url,
      body,
      headers: _headers,
    ).transform((_) => true);
    _networkStatusService.finishGalleryLoading();
    return result.execErrorCast();
  }
}

class CirclesClient extends FriendicaClient {
  static final _logger = Logger('$CirclesClient');

  CirclesClient(super.credentials) : super();

  FutureResult<List<CircleData>, ExecError> getCircles() async {
    _logger.finest(() => 'Getting circle (Mastodon List) data');
    final url = 'https://$serverName/api/v1/lists';
    final request = Uri.parse(url);
    return (await _getApiListRequest(request).andThenSuccessAsync(
            (listsJson) async => listsJson.data
                .map((json) => CircleDataMastodonExtensions.fromJson(json))
                .toList()))
        .mapError((error) => error is ExecError
            ? error
            : ExecError(type: ErrorType.localError, message: error.toString()));
  }

  FutureResult<PagedResponse<List<Connection>>, ExecError> getCircleMembers(
    CircleData circleData,
    PagingData page,
  ) async {
    _networkStatusService.startConnectionUpdateStatus();
    _logger.finest(() =>
        'Getting members for circle (Mastodon List) of name ${circleData.name} with paging: $page');
    final baseUrl =
        'https://$serverName/api/v1/lists/${circleData.id}/accounts';
    final url = Uri.parse('$baseUrl?${page.toQueryParameters()}');
    final result = await _getApiPagedRequest(url);
    _networkStatusService.finishConnectionUpdateStatus();
    return result
        .andThenSuccess((response) => response.map((jsonArray) =>
            (jsonArray as List<dynamic>)
                .map((json) => ConnectionMastodonExtensions.fromJson(json))
                .toList()))
        .execErrorCast();
  }

  FutureResult<CircleData, ExecError> createCircle(String title) async {
    _logger.finest(() => 'Creating circle (Mastodon List) of name $title');
    final url = 'https://$serverName/api/v1/lists';
    final body = {
      'title': title,
    };
    final result = await postUrl(
      Uri.parse(url),
      body,
      headers: _headers,
    ).andThenSuccessAsync((data) async =>
        CircleDataMastodonExtensions.fromJson(jsonDecode(data)));
    return result.execErrorCast();
  }

  FutureResult<CircleData, ExecError> renameCircle(
      String id, String title) async {
    _logger.finest(() => 'Reanming circle (Mastodon List) to name $title');
    final url = 'https://$serverName/api/v1/lists/$id';
    final body = {
      'title': title,
    };
    final result = await putUrl(
      Uri.parse(url),
      body,
      headers: _headers,
    ).andThenSuccessAsync((data) async {
      final json = jsonDecode(data);
      return CircleDataMastodonExtensions.fromJson(json);
    });
    return result.execErrorCast();
  }

  FutureResult<bool, ExecError> deleteCircle(CircleData circleData) async {
    _logger.finest(
        () => 'Reanming circle (Mastodon List) to name ${circleData.name}');
    final url = 'https://$serverName/api/v1/lists/${circleData.id}';
    final result = await deleteUrl(Uri.parse(url), {}, headers: _headers);
    return result.mapValue((_) => true).execErrorCast();
  }

  FutureResult<List<CircleData>, ExecError> getMemberCirclesForConnection(
      String connectionId) async {
    _logger.finest(() =>
        'Getting circles (Mastodon Lists) containing connection: $connectionId');
    final url = 'https://$serverName/api/v1/accounts/$connectionId/lists';
    final request = Uri.parse(url);
    return (await _getApiListRequest(request).andThenSuccessAsync(
            (listsJson) async => listsJson.data
                .map((json) => CircleDataMastodonExtensions.fromJson(json))
                .toList()))
        .mapError((error) => error as ExecError);
  }

  FutureResult<bool, ExecError> addConnectionToCircle(
    CircleData circle,
    Connection connection,
  ) async {
    _logger.finest(() => 'Adding connection to circle');
    final url = 'https://$serverName/api/v1/lists/${circle.id}/accounts';
    final request = Uri.parse(url);
    final requestData = {
      'account_ids': [connection.id]
    };
    return (await postUrl(request, requestData, headers: _headers))
        .mapValue((_) => true);
  }

  FutureResult<bool, ExecError> removeConnectionFromCircle(
    CircleData circle,
    Connection connection,
  ) async {
    _logger.finest(() => 'Adding connection to circle');
    final url = 'https://$serverName/api/v1/lists/${circle.id}/accounts';
    final request = Uri.parse(url);
    final requestData = {
      'account_ids': [connection.id]
    };
    return (await deleteUrl(request, requestData, headers: _headers))
        .mapValue((_) => true);
  }
}

class ImageClient extends FriendicaClient {
  ImageClient(super.credentials) : super();

  FutureResult<ImageEntry, ExecError> editImageData(ImageEntry image) async {
    _networkStatusService.startGalleryLoading();
    final uri = Uri.parse('https://$serverName/api/friendica/photo/update');
    final body = {
      'album': image.album,
      'desc': image.description,
      'photo_id': image.id,
    };

    final result = await postUrl(uri, body, headers: _headers)
        .andThen((_) => Result.ok(image));
    _networkStatusService.finishGalleryLoading();
    return result.execErrorCast();
  }

  FutureResult<ImageEntry, ExecError> deleteImage(ImageEntry image) async {
    final uri = Uri.parse(
      'https://$serverName/api/friendica/photo/delete?photo_id=${image.id}',
    );

    final result = await postUrl(uri, {}, headers: _headers)
        .andThen((_) => Result.ok(image));
    return result.execErrorCast();
  }
}

class InteractionsClient extends FriendicaClient {
  static final _logger = Logger('$InteractionsClient');

  InteractionsClient(super.credentials) : super();

  // TODO Convert getLikes to using paging for real
  FutureResult<List<Connection>, ExecError> getLikes(String id) async {
    _networkStatusService.startInteractionsLoading();
    final result = (await runCatchingAsync(() async {
      final url = 'https://$serverName/api/v1/statuses/$id/favourited_by';
      final request = Uri.parse(url);
      _logger.finest(() => 'Getting favorites for status $id');
      return (await _getApiListRequest(request)
          .andThenSuccessAsync((jsonArray) async {
        return jsonArray.data
            .map((p) => ConnectionMastodonExtensions.fromJson(p))
            .toList();
      }));
    }))
        .execErrorCastAsync();
    _networkStatusService.finishInteractionsLoading();
    return result;
  }

  // TODO Convert getReshares to using paging for real
  FutureResult<List<Connection>, ExecError> getReshares(String id) async {
    _networkStatusService.startInteractionsLoading();
    final result = (await runCatchingAsync(() async {
      final url = 'https://$serverName/api/v1/statuses/$id/reblogged_by';
      final request = Uri.parse(url);
      _logger.finest(() => 'Getting rebloggers for status $id');
      return (await _getApiListRequest(request)
          .andThenSuccessAsync((jsonArray) async {
        return jsonArray.data
            .map((p) => ConnectionMastodonExtensions.fromJson(p))
            .toList();
      }));
    }))
        .execErrorCastAsync();
    _networkStatusService.finishInteractionsLoading();
    return result;
  }

  FutureResult<TimelineEntry, ExecError> changeFavoriteStatus(
      String id, bool status) async {
    final action = status ? 'favourite' : 'unfavourite';
    final url = Uri.parse('https://$serverName/api/v1/statuses/$id/$action');
    final result = await postUrl(url, {}, headers: _headers);
    if (result.isFailure) {
      return result.errorCast();
    }

    final responseText = result.value;

    return runCatching<TimelineEntry>(() {
      final json = jsonDecode(responseText);
      return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
    }).mapError((error) {
      return ExecError(type: ErrorType.parsingError, message: error.toString());
    });
  }
}

class InstanceDataClient extends FriendicaClient {
  static final _logger = Logger('$InteractionsClient');

  InstanceDataClient(super.credentials);

  FutureResult<InstanceInfo, ExecError> getInstanceData() async {
    _logger.finest(() => 'Getting $serverName instance info');
    final v2Result = await getInstanceDataV2();
    if (v2Result.isSuccess) {
      return v2Result;
    }

    return getInstanceDataV1();
  }

  FutureResult<InstanceInfo, ExecError> getInstanceDataV1() async {
    _logger.finest(() => 'Getting $serverName instance info via V1 endpoint');
    final url = Uri.parse('https://$serverName/api/v1/instance');
    final result = await _getApiRequest(url);
    return result.andThen((json) {
      return fromInstanceV1Json(json);
    }).execErrorCast();
  }

  FutureResult<InstanceInfo, ExecError> getInstanceDataV2() async {
    _logger.finest(() => 'Getting $serverName instance info via V2 endpoint');
    final url = Uri.parse('https://$serverName/api/v2/instance');
    final result = await _getApiRequest(url);
    return result.andThen((response) {
      final instanceInfoResult = fromInstanceV2Json(response);
      return instanceInfoResult;
    }).execErrorCast();
  }
}

class NotificationsClient extends FriendicaClient {
  static final _logger = Logger('$NotificationsClient');

  NotificationsClient(super.credentials) : super();

  FutureResult<PagedResponse<List<UserNotification>>, ExecError>
      getNotifications(PagingData page, bool includeAll) async {
    _networkStatusService.startNotificationUpdate();
    final url =
        'https://$serverName/api/v1/notifications?include_all=$includeAll';
    final request = Uri.parse('$url&${page.toQueryParameters()}');
    _logger.finest(() => 'Getting new notifications');
    final result =
        await _getApiListRequest(request).transformAsync((response) async {
      final notifications = <UserNotification>[];

      final st = Stopwatch()..start();
      for (final json in response.data) {
        if (st.elapsedMilliseconds > maxProcessingMillis) {
          await Future.delayed(processingSleep, () => st.reset());
        }
        notifications.add(NotificationMastodonExtension.fromJson(json));
      }
      return PagedResponse(notifications,
          id: response.id, previous: response.previous, next: response.next);
    });
    _networkStatusService.finishNotificationUpdate();
    return result.execErrorCast();
  }

  FutureResult<bool, ExecError> clearNotifications() async {
    final url = 'https://$serverName/api/v1/notifications/clear';
    final request = Uri.parse(url);
    _logger.finest(() => 'Clearing unread notification');
    final response = await postUrl(request, {}, headers: _headers);
    return response.mapValue((value) => true);
  }

  FutureResult<bool, ExecError> clearNotification(
      UserNotification notification) async {
    final url =
        'https://$serverName/api/v1/notifications/${notification.id}/dismiss';
    final request = Uri.parse(url);
    _logger.fine(() => 'Clearing unread notification for $notification');
    final response = await postUrl(request, {}, headers: _headers);
    return response.mapValue((value) => true);
  }
}

class RelationshipsClient extends FriendicaClient {
  static final _logger = Logger('$RelationshipsClient');

  RelationshipsClient(super.credentials) : super();

  FutureResult<PagedResponse<List<Connection>>, ExecError> getBlocks(
      PagingData page) async {
    _networkStatusService.startNotificationUpdate();
    final url = 'https://$serverName/api/v1/blocks';
    final request = Uri.parse('$url&${page.toQueryParameters()}');
    _logger.finest(() => 'Getting blocks for $page');
    final result =
        await _getApiListRequest(request).transformAsync((response) async {
      final blocks = <Connection>[];

      final st = Stopwatch()..start();
      for (final json in response.data) {
        if (st.elapsedMilliseconds > maxProcessingMillis) {
          await Future.delayed(processingSleep, () => st.reset());
        }
        blocks.add(
          ConnectionMastodonExtensions.fromJson(json)
              .copy(status: ConnectionStatus.blocked),
        );
      }
      return PagedResponse(blocks,
          id: response.id, previous: response.previous, next: response.next);
    });
    _networkStatusService.finishNotificationUpdate();
    return result.execErrorCast();
  }

  FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowing(
      PagingData page) async {
    _logger.fine(() => 'Getting following with paging data $page');
    _networkStatusService.startConnectionUpdateStatus();
    final myId = profile.userId;
    final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
    final result = await _getApiListRequest(
      Uri.parse('$baseUrl/following?${page.toQueryParameters()}'),
    );
    _networkStatusService.finishConnectionUpdateStatus();
    return result
        .andThenSuccess((response) => response.map((jsonArray) => jsonArray
            .map((json) => ConnectionMastodonExtensions.fromJson(json))
            .toList()))
        .execErrorCast();
  }

  FutureResult<PagedResponse<List<FollowRequest>>, ExecError> getFollowRequests(
      PagingData page) async {
    _logger.finest(() => 'Getting follow requests with paging data $page');
    _networkStatusService.startConnectionUpdateStatus();
    final baseUrl = 'https://$serverName/api/v1/follow_requests';
    final result = await _getApiListRequest(
      Uri.parse('$baseUrl?${page.toQueryParameters()}'),
    );
    _networkStatusService.finishConnectionUpdateStatus();
    return result
        .andThenSuccess((response) => response.map((jsonArray) => jsonArray
            .map((json) => FollowRequestMastodonExtension.fromJson(json))
            .toList()))
        .execErrorCast();
  }

  FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowers(
      PagingData page) async {
    _logger.finest(() => 'Getting followers data with page data $page');
    _networkStatusService.startConnectionUpdateStatus();
    final myId = profile.userId;
    final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
    final result1 = await _getApiListRequest(
      Uri.parse('$baseUrl/followers?${page.toQueryParameters()}'),
    );

    _networkStatusService.finishConnectionUpdateStatus();
    return result1
        .andThenSuccess((response) => response.map((jsonArray) => jsonArray
            .map((json) => ConnectionMastodonExtensions.fromJson(json))
            .toList()))
        .execErrorCast();
  }

  FutureResult<Connection, ExecError> getConnectionWithStatus(
      Connection connection) async {
    _logger.finest(() => 'Getting circle (Mastodon List) data');
    _networkStatusService.startConnectionUpdateStatus();
    final myId = profile.userId;
    final id = int.parse(connection.id);
    final paging = '?min_id=${id - 1}&max_id=${id + 1}';
    final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
    final following =
        await _getApiListRequest(Uri.parse('$baseUrl/following$paging')).fold(
            onSuccess: (followings) => followings.data.isNotEmpty,
            onError: (error) {
              _logger.severe('Error getting following list: $error');
              return false;
            });
    final follower =
        await _getApiListRequest(Uri.parse('$baseUrl/followers$paging')).fold(
            onSuccess: (followings) => followings.data.isNotEmpty,
            onError: (error) {
              _logger.severe('Error getting follower list: $error');
              return false;
            });

    var status = ConnectionStatus.none;
    if (following && follower) {
      status = ConnectionStatus.mutual;
    } else if (following) {
      status = ConnectionStatus.youFollowThem;
    } else if (follower) {
      status = ConnectionStatus.theyFollowYou;
    }

    _networkStatusService.finishConnectionUpdateStatus();
    return Result.ok(connection.copy(status: status));
  }

  FutureResult<PagedResponse<List<Connection>>, ExecError>
      getConnectionRequests(PagingData page) async {
    _logger.finest(() => 'Getting connection requests with page data $page');
    _networkStatusService.startConnectionUpdateStatus();
    final baseUrl = 'https://$serverName/api/v1/follow_requests';
    final result1 = await _getApiListRequest(
      Uri.parse('$baseUrl&${page.toQueryParameters()}'),
    );

    _networkStatusService.finishConnectionUpdateStatus();
    return result1
        .andThenSuccess((response) => response.map((jsonArray) => jsonArray
            .map((json) => ConnectionMastodonExtensions.fromJson(json))
            .toList()))
        .execErrorCast();
  }

  FutureResult<Connection, ExecError> acceptFollow(
      Connection connection) async {
    final id = connection.id;
    final url =
        Uri.parse('https://$serverName/api/v1/follow_requests/$id/authorize');
    final result = await postUrl(url, {}, headers: _headers)
        .andThenSuccessAsync((jsonString) async {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.mapError((error) => error is ExecError
        ? error
        : ExecError(type: ErrorType.localError, message: error.toString()));
  }

  FutureResult<Connection, ExecError> rejectFollow(
      Connection connection) async {
    final id = connection.id;
    final url =
        Uri.parse('https://$serverName/api/v1/follow_requests/$id/reject');
    final result = await postUrl(url, {}, headers: _headers)
        .andThenSuccessAsync((jsonString) async {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.mapError((error) => error is ExecError
        ? error
        : ExecError(type: ErrorType.localError, message: error.toString()));
  }

  FutureResult<Connection, ExecError> ignoreFollow(
      Connection connection) async {
    final id = connection.id;
    final url =
        Uri.parse('https://$serverName/api/v1/follow_requests/$id/ignore');
    final result = await postUrl(url, {}, headers: _headers)
        .andThenSuccessAsync((jsonString) async {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.mapError((error) => error is ExecError
        ? error
        : ExecError(type: ErrorType.localError, message: error.toString()));
  }

  FutureResult<Connection, ExecError> blockConnection(
      Connection connection) async {
    final id = connection.id;
    final url = Uri.parse('https://$serverName/api/v1/accounts/$id/block');
    final result = await postUrl(url, {}, headers: _headers).transform(
      (_) => connection.copy(status: ConnectionStatus.blocked),
    );
    return result.execErrorCast();
  }

  FutureResult<Connection, ExecError> unblockConnection(
      Connection connection) async {
    final id = connection.id;
    final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unblock');
    final result = await postUrl(url, {}, headers: _headers)
        .transformAsync((jsonString) async {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.execErrorCast();
  }

  FutureResult<Connection, ExecError> followConnection(
      Connection connection) async {
    final id = connection.id;
    final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow');
    final result =
        await postUrl(url, {}, headers: _headers).transform((jsonString) {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.execErrorCast();
  }

  FutureResult<Connection, ExecError> unFollowConnection(
      Connection connection) async {
    final id = connection.id;
    final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow');
    final result =
        await postUrl(url, {}, headers: _headers).transform((jsonString) {
      return _updateConnectionFromFollowRequestResult(connection, jsonString);
    });
    return result.execErrorCast();
  }

  Connection _updateConnectionFromFollowRequestResult(
      Connection connection, String jsonString) {
    final json = jsonDecode(jsonString) as Map<String, dynamic>;
    final theyFollowYou = json['followed_by'] ?? 'false';
    final youFollowThem = json['following'] ?? 'false';
    late final ConnectionStatus newStatus;
    if (theyFollowYou && youFollowThem) {
      newStatus = ConnectionStatus.mutual;
    } else if (theyFollowYou) {
      newStatus = ConnectionStatus.theyFollowYou;
    } else if (youFollowThem) {
      newStatus = ConnectionStatus.youFollowThem;
    } else {
      newStatus = ConnectionStatus.none;
    }
    return connection.copy(status: newStatus);
  }
}

class RemoteFileClient extends FriendicaClient {
  static final _logger = Logger('$RemoteFileClient');

  RemoteFileClient(super.credentials) : super();

  FutureResult<Uint8List, ExecError> getFileBytes(Uri url) async {
    _logger.finest('GET: $url');
    try {
      final response = await http.get(
        url,
        headers: {
          'Authorization': _profile.credentials.authHeaderValue,
        },
      );

      if (response.statusCode != 200) {
        return Result.error(ExecError(
            type: ErrorType.authentication,
            message: '${response.statusCode}: ${response.reasonPhrase}'));
      }
      return Result.ok(response.bodyBytes);
    } catch (e) {
      return Result.error(
          ExecError(type: ErrorType.localError, message: e.toString()));
    }
  }

  FutureResult<ImageEntry, ExecError> uploadFileAsAttachment({
    required List<int> bytes,
    String description = '',
    String album = '',
    String fileName = '',
    required Visibility visibility,
  }) async {
    final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
    final request = http.MultipartRequest('POST', postUri);
    request.headers['Authorization'] = _profile.credentials.authHeaderValue;
    if (usePhpDebugging) {
      request.headers['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
    }
    request.fields['desc'] = description;
    request.fields['album'] = album;
    request.fields.addAll(visibility.toMapEntries());
    request.files.add(http.MultipartFile.fromBytes(
        'media',
        filename: fileName,
        contentType:
            MediaType.parse('image/${ImageTypes.fromExtension(fileName).name}'),
        bytes));
    final response = await request.send();
    final body = utf8.decode(await response.stream.toBytes());
    if (response.statusCode != 200) {
      return Result.error(
        ExecError(
          type: ErrorType.missingEndpoint,
          message: body,
        ),
      );
    }

    final imageDataJson = jsonDecode(body);
    final newImageData = ImageEntryFriendicaExtension.fromJson(imageDataJson);

    return Result.ok(newImageData);
  }
}

class ProfileClient extends FriendicaClient {
  static final _logger = Logger('$ProfileClient');

  ProfileClient(super.credentials) : super();

  FutureResult<Connection, ExecError> getMyProfile() async {
    _logger.finest(() => 'Getting logged in user profile');
    final request =
        Uri.parse('https://$serverName/api/v1/accounts/verify_credentials');
    return (await _getApiRequest(request, timeout: oauthTimeout))
        .mapValue((json) => ConnectionMastodonExtensions.fromJson(
              json,
              defaultServerName: serverName,
            ).copy(
              status: ConnectionStatus.you,
              network: 'friendica',
            ));
  }
}

class StatusesClient extends FriendicaClient {
  static final _logger = Logger('$StatusesClient');

  StatusesClient(super.credentials) : super();

  // TODO Convert getPostOrComment to using paging for real
  FutureResult<List<TimelineEntry>, ExecError> getPostOrComment(String id,
      {bool fullContext = false}) async {
    _networkStatusService.startTimelineLoading();
    final result = (await runCatchingAsync(() async {
      final baseUrl = 'https://$serverName/api/v1/statuses/$id';
      final url = fullContext ? '$baseUrl/context' : baseUrl;
      final request = Uri.parse('$url?limit=1000');
      _logger.finest(() =>
          'Getting entry for status $id, full context? $fullContext : $url');
      return (await _getApiRequest(request).andThenSuccessAsync((json) async {
        if (fullContext) {
          final ancestors = json['ancestors'] as List<dynamic>;
          final descendants = json['descendants'] as List<dynamic>;
          final items = [
            ...ancestors
                .map((a) => TimelineEntryMastodonExtensions.fromJson(a)),
            ...descendants
                .map((d) => TimelineEntryMastodonExtensions.fromJson(d))
          ];
          _logger.finer(() =>
              'Got status $id with full context which returned ${ancestors.length} ancestors and ${descendants.length} descendants');
          return items;
        } else {
          return [TimelineEntryMastodonExtensions.fromJson(json)];
        }
      }));
    }));
    _networkStatusService.finishTimelineLoading();

    return result.execErrorCast();
  }

  FutureResult<TimelineEntry, ExecError> createNewStatus({
    required String text,
    String spoilerText = '',
    String inReplyToId = '',
    List<String> mediaIds = const [],
    required Visibility visibility,
  }) async {
    _logger.finest(() =>
        'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId, with media: $mediaIds');
    final url = Uri.parse('https://$serverName/api/v1/statuses');
    final body = {
      'status': text,
      if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
      if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
      if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
      'visibility': visibility.toCreateStatusValue(inReplyToId.isNotEmpty),
      'friendica': {
        'title': '',
      },
    };
    final result = await postUrl(url, body, headers: _headers);
    if (result.isFailure) {
      return result.errorCast();
    }

    final responseText = result.value;

    return runCatching<TimelineEntry>(() {
      final json = jsonDecode(responseText);
      return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
    }).mapError((error) {
      return ExecError(type: ErrorType.parsingError, message: error.toString());
    });
  }

  FutureResult<TimelineEntry, ExecError> editStatus({
    required String id,
    required String text,
    String spoilerText = '',
    List<String> mediaIds = const [],
  }) async {
    _logger.finest(() => 'Updating status $id');
    final url = Uri.parse('https://$serverName/api/v1/statuses/$id');
    final body = {
      'status': text,
      if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
      if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
      'friendica': {
        'title': '',
      },
    };
    final result = await putUrl(url, body, headers: _headers);
    if (result.isFailure) {
      return result.errorCast();
    }

    final responseText = result.value;

    return runCatching<TimelineEntry>(() {
      final json = jsonDecode(responseText);
      return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
    }).mapError((error) {
      return ExecError(type: ErrorType.parsingError, message: error.toString());
    });
  }

  FutureResult<TimelineEntry, ExecError> resharePost(String id) async {
    _logger.finest(() => 'Reshare post $id');
    final url = Uri.parse('https://$serverName/api/v1/statuses/$id/reblog');
    final result = await postUrl(url, {}, headers: _headers);
    if (result.isFailure) {
      return result.errorCast();
    }

    final responseText = result.value;

    return runCatching<TimelineEntry>(() {
      final json = jsonDecode(responseText);
      return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
    }).mapError((error) {
      return ExecError(type: ErrorType.parsingError, message: error.toString());
    });
  }

  FutureResult<TimelineEntry, ExecError> unResharePost(String id) async {
    _logger.finest(() => 'Reshare post $id');
    final url = Uri.parse('https://$serverName/api/v1/statuses/$id/unreblog');
    final result = await postUrl(url, {}, headers: _headers);
    if (result.isFailure) {
      return result.errorCast();
    }

    final responseText = result.value;

    return runCatching<TimelineEntry>(() {
      final json = jsonDecode(responseText);
      return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
    }).mapError((error) {
      return ExecError(type: ErrorType.parsingError, message: error.toString());
    });
  }

  FutureResult<bool, ExecError> deleteEntryById(String id) async {
    _logger.finest(() => 'Deleting post/comment $id');
    final url = 'https://$serverName/api/v1/statuses/$id';
    final request = Uri.parse(url);
    return (await deleteUrl(
      request,
      {},
      headers: _headers,
    ))
        .mapValue((_) => true);
  }
}

class SearchClient extends FriendicaClient {
  static final _logger = Logger('$StatusesClient');

  SearchClient(super.credentials) : super();

  FutureResult<PagedResponse<SearchResults>, ExecError> search(
      SearchTypes type, String searchTerm, PagingData page) async {
    _logger.finest(() => 'Searching $type for  term: $searchTerm');

    if (type == SearchTypes.directLink) {
      final isFediverseResult = await getIt<FediverseServiceValidator>()
          .checkIfFediverseLink(searchTerm);
      if (isFediverseResult.isFailure) {
        return isFediverseResult.errorCast();
      }

      if (!isFediverseResult.value) {
        return buildErrorResult(
          type: ErrorType.parsingError,
          message: 'URL appears to not be to a fediverse server: $searchTerm',
        );
      }
    }
    _networkStatusService.startSearchLoading();
    final url =
        'https://$serverName/api/v1/search?${page.toQueryParameters()}&${type.toQueryParameters()}&q=$searchTerm';
    final result = await _getApiPagedRequest(
      Uri.parse(url),
    );

    _networkStatusService.finishSearchLoaing();
    return result
        .andThenSuccess((response) => response
            .map((json) => SearchResultMastodonExtensions.fromJson(json)))
        .execErrorCast();
  }
}

class TimelineClient extends FriendicaClient {
  static final _logger = Logger('$TimelineClient');

  TimelineClient(super.credentials) : super();

  // TODO Convert User Timeline to using paging for real
  FutureResult<List<TimelineEntry>, ExecError> getUserTimeline(
      {String userId = '', int page = 1, int count = 10}) async {
    _logger.finest(() => 'Getting user timeline for $userId');
    _networkStatusService.startTimelineLoading();
    final baseUrl = 'https://$serverName/api/statuses/user_timeline?';
    final pagingData = 'count=$count&page=$page';
    final url = userId.isEmpty
        ? '$baseUrl$pagingData'
        : '${baseUrl}screen_name=$userId$pagingData';
    final request = Uri.parse(url);
    final result = await _getApiListRequest(request).transformAsync(
      (postsJson) async => await _timelineEntriesFromJson(postsJson.data),
    );

    _networkStatusService.finishTimelineLoading();
    return result.execErrorCast();
  }

  FutureResult<List<TimelineEntry>, ExecError> getTimeline(
      {required TimelineIdentifiers type, required PagingData page}) async {
    _networkStatusService.startTimelineLoading();
    final String timelinePath = _typeToTimelinePath(type);
    final String timelineQPs = _typeToTimelineQueryParameters(type);
    final baseUrl = 'https://$serverName/api/v1/$timelinePath';
    final url =
        '$baseUrl?exclude_replies=true&${page.toQueryParameters()}&$timelineQPs';
    final request = Uri.parse(url);
    _logger.finest(() => 'Getting ${type.toHumanKey()} with paging data $page');
    final result = await _getApiListRequest(request).transformAsync(
      (postsJson) async => await _timelineEntriesFromJson(postsJson.data),
    );
    _networkStatusService.finishTimelineLoading();
    return result.execErrorCast();
  }

  Future<List<TimelineEntry>> _timelineEntriesFromJson(
    List<dynamic> postsJson,
  ) async {
    final finalResult = <TimelineEntry>[];
    final st = Stopwatch()..start();
    for (final json in postsJson) {
      if (st.elapsedMilliseconds > maxProcessingMillis) {
        await Future.delayed(processingSleep, () => st.reset());
      }
      finalResult.add(TimelineEntryMastodonExtensions.fromJson(json));
    }

    return finalResult;
  }

  String _typeToTimelinePath(TimelineIdentifiers type) {
    switch (type.timeline) {
      case TimelineType.home:
        return 'timelines/home';
      case TimelineType.global:
        return 'timelines/public';
      case TimelineType.local:
        return 'timelines/public';
      case TimelineType.circle:
        return 'timelines/list/${type.auxData}';
      case TimelineType.profile:
        return '/accounts/${type.auxData}/statuses';
      case TimelineType.self:
        final myId = profile.userId;
        return '/accounts/$myId/statuses';
    }
  }

  String _typeToTimelineQueryParameters(TimelineIdentifiers type) {
    switch (type.timeline) {
      case TimelineType.home:
      case TimelineType.global:
      case TimelineType.profile:
      case TimelineType.circle:
      case TimelineType.self:
        return '';
      case TimelineType.local:
        return 'local=true';
    }
  }
}

abstract class FriendicaClient {
  final Profile _profile;

  late final NetworkStatusService _networkStatusService;

  String get serverName => _profile.serverName;

  Profile get profile => _profile;

  FriendicaClient(Profile credentials) : _profile = credentials {
    _networkStatusService = getIt<NetworkStatusService>();
  }

  FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
    Uri url, {
    Duration? timeout,
  }) async {
    return await getUrl(
      url,
      headers: _headers,
      timeout: timeout,
    ).transformAsync(
      (response) async {
        return response.map((data) => jsonDecode(data) as List<dynamic>);
      },
    ).execErrorCastAsync();
  }

  FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(Uri url,
      {Duration? timeout}) async {
    return await getUrl(
      url,
      headers: _headers,
      timeout: timeout,
    ).transformAsync(
      (response) async {
        return response.map((data) => jsonDecode(data));
      },
    ).execErrorCastAsync();
  }

  FutureResult<dynamic, ExecError> _getApiRequest(Uri url,
      {Duration? timeout}) async {
    return await getUrl(
      url,
      headers: _headers,
      timeout: timeout,
    ).transformAsync(
      (response) async {
        return jsonDecode(response.data);
      },
    ).execErrorCastAsync();
  }

  Map<String, String> get _headers => {
        'Authorization': _profile.credentials.authHeaderValue,
        'Content-Type': 'application/json; charset=UTF-8',
      };
}
