library fever_api;

import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'feed.dart';
part 'item.dart';
part 'group.dart';
part 'favicon.dart';
part 'feeds_group.dart';
part 'cache.dart';
part 'exceptions.dart';
part 'util.dart';

/// Basic implementation of the Fever API
/// https://github.com/dasmurphy/tinytinyrss-fever-plugin/blob/master/fever-api.md

class FeverAPI {
  String? apiKey;
  String? apiUrl;

  late final APICache cache;

  /// We use a shared HTTP client for all requests
  final _httpClient = http.Client();

  /// If set, the `newestKnownPost` value will be updated automatically.
  /// This is used for notifications.
  SharedPreferences? sharedPrefs;

  FeverAPI({this.apiKey, this.apiUrl, this.sharedPrefs}) {
    cache = APICache(this);
  }

  static String generateApiKey(String username, String password) {
    var bytes = utf8.encode('$username:$password');
    return md5.convert(bytes).toString();
  }

  /// Checks whether the given API URL is a valid Fever API endpoint
  static Future<bool> isValidAPI(String url) async {
    try {
      final response = jsonDecode((await http.post(Uri.parse(url))).body);
      return response['api_version'] == 3;
    } catch (e) {
      print(e);
      return false;
    }
  }

  /// Checks whether the given API URL is a valid Fever API endpoint and the given API key is authorized to access it
  static Future<bool> isAuthenticated(String url, String key) async {
    try {
      final response = jsonDecode(
          (await http.post(Uri.parse(url), body: {'api_key': key})).body);
      return response['api_version'] == 3 && response['auth'] == 1;
    } catch (e) {
      print(e);
      return false;
    }
  }

  /// Whether we have an API URL and API key set
  bool loggedIn() {
    return apiKey != null && apiUrl != null;
  }

  APIRequestBuilder request() {
    return APIRequestBuilder(this, _httpClient, sharedPrefs);
  }
}

class APIRequestBuilder {
  final FeverAPI api;
  final SharedPreferences? _sharedPrefs;
  final http.Client _httpClient;

  List<String> args = [];
  Item? _markedItem;
  ItemMarkType? _markedItemAs;

  APIRequestBuilder(this.api, this._httpClient, this._sharedPrefs);

  void _addArg(String arg) {
    if (!args.contains(arg)) args.add(arg);
  }

  Uri generateUrl() {
    return Uri.parse('${api.apiUrl}?api&${args.join('&')}');
  }

  /// Include items in response
  APIRequestBuilder withItems() {
    _addArg('items');
    return this;
  }

  /// Include feeds in response
  APIRequestBuilder withFeeds() {
    _addArg('feeds');
    return this;
  }

  /// Include groups in response
  APIRequestBuilder withGroups() {
    _addArg('groups');
    return this;
  }

  APIRequestBuilder withFavicons() {
    _addArg("favicons");
    return this;
  }

  /// Mark single item as read, unread, saved, unsaved
  APIRequestBuilder markItem(Item item, ItemMarkType markAs) {
    _addArg('mark=item');
    _addArg('as=${markAs.name}');
    _addArg('id=${item.id}');
    _markedItem = item;
    _markedItemAs = markAs;
    return this;
  }

  /// Use the `since_id` argument with the highest id of locally cached items to
  /// request 50 additional items. Repeat until the items array in the response
  /// is empty.
  APIRequestBuilder sinceId(int id) {
    _addArg('since_id=$id');
    return this;
  }

  /// Use the `max_id` argument with the lowest id of locally cached items (or
  /// 0 initially) to request 50 previous items. Repeat until the items array
  /// in the response is empty.
  APIRequestBuilder maxId(int id) {
    _addArg('max_id=$id');
    return this;
  }

  APIRequestBuilder withIds(List<int> ids) {
    _addArg('with_ids=${ids.join(',')}');
    return this;
  }

  /// Executes the API request and returns the JSON data
  Future<Map<String, dynamic>> fetch() async {
    if (api.apiKey == null || api.apiUrl == null) {
      throw Exception('API Key or API URL not set');
    }

    var url = generateUrl();
    print('Fetching $url');

    final response = await _httpClient.post(url, body: {'api_key': api.apiKey});
    final data = jsonDecode((response).body);
    if (data['auth'] == 0) throw UnauthenticatedException(data['auth']);
    return data;
  }

  /// Executes the API request and populates the local cache with the response data
  Future<Map<String, dynamic>> execute() async {
    final data = await fetch();

    if (data.containsKey('groups')) {
      List<Group> groups = (data['groups'] as List<dynamic>)
          .map((json) => Group.fromJSON(api, json))
          .toList();

      for (var group in groups) {
        api.cache.groups.set(group.id, group);
      }
    }

    if (data.containsKey('feeds')) {
      List<Feed> feeds = (data['feeds'] as List<dynamic>)
          .map((json) => Feed.fromJSON(api, json))
          .toList();

      for (var feed in feeds) {
        api.cache.feeds.set(feed.id, feed);
      }
    }

    if (data.containsKey('items')) {
      var currentHighest = _sharedPrefs?.getInt('newestKnownPost') ?? 0;

      List<Item> items = (data['items'] as List<dynamic>)
          .map((json) => Item.fromJSON(api, json))
          .toList();

      for (var item in items) {
        if (item.id > currentHighest) {
          currentHighest = item.id;
        }

        api.cache.items.set(item.id, item);
        _sharedPrefs?.setInt('newestKnownPost', currentHighest);
      }
    }

    if (data.containsKey('favicons')) {
      List<Favicon> favicons = (data['favicons'] as List<dynamic>)
          .map((json) => Favicon.fromJSON(api, json))
          .toList();

      for (var favicon in favicons) {
        api.cache.favicons.set(favicon.id, favicon);
      }
    }

    if (data.containsKey('feeds_groups')) {
      List<FeedsGroup> feedsGroups = (data['feeds_groups'] as List<dynamic>)
          .map((json) => FeedsGroup.fromJSON(api, json))
          .toList();

      api.cache.feedsGroups.clear();
      api.cache.feedsGroups.addAll(feedsGroups);
    }

    if (_markedItem != null && _markedItemAs != null) {
      switch (_markedItemAs!) {
        case ItemMarkType.read:
          _markedItem!.isRead = true;
          break;
        case ItemMarkType.unread:
          _markedItem!.isRead = false;
          break;
        case ItemMarkType.saved:
          _markedItem!.isSaved = true;
          break;
        case ItemMarkType.unsaved:
          _markedItem!.isSaved = false;
          break;
      }
      api.cache.items.set(_markedItem!.id, _markedItem!);
    }

    return data;
  }
}

enum ItemMarkType {
  read,
  unread,
  saved,
  unsaved,
}
