import 'dart:convert';

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

import '../friendica_client/paged_response.dart';
import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';

final _logger = Logger('NetworkUtils');

http.Response requestTimeout() => http.Response('Client side timeout', 408);

enum _RequestType {
  get,
}

const _expireDuration = Duration(seconds: 2);

class _CachedResponse {
  final _RequestType requestType;
  final Uri requestUri;
  final Map<String, dynamic> requestBody;
  final Map<String, String> headers;
  final http.Response response;
  final DateTime requestTime;

  _CachedResponse(
      {required this.requestType,
      required this.requestUri,
      required this.requestBody,
      required this.headers,
      required this.response,
      required this.requestTime});

  factory _CachedResponse.requestStub(_RequestType type, Uri uri,
          Map<String, String>? headers, Map<String, dynamic>? body) =>
      _CachedResponse(
        requestType: type,
        requestUri: uri,
        requestBody: body ?? {},
        headers: headers ?? {},
        response: http.Response('', 555),
        requestTime: DateTime(0),
      );

  factory _CachedResponse.response(
    _RequestType type,
    Uri uri,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
    http.Response response,
  ) =>
      _CachedResponse(
        requestType: type,
        requestUri: uri,
        requestBody: body ?? {},
        headers: headers ?? {},
        response: response,
        requestTime: DateTime.now(),
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is _CachedResponse &&
          runtimeType == other.runtimeType &&
          requestType == other.requestType &&
          requestUri == other.requestUri;

  @override
  int get hashCode => requestType.hashCode ^ requestUri.hashCode;
}

class _ExpiringRequestCache {
  final _responses = <_CachedResponse, _CachedResponse>{};

  void _cleanupCache() {
    final expireTime = DateTime.now().subtract(_expireDuration);
    _logger
        .finest('Cleaning up request cache with ${_responses.length} entries');
    _responses.removeWhere((key, value) {
      final expired = key.requestTime.isBefore(expireTime);
      if (expired) {
        _logger.finest(
            'Expiring request: ${value.requestType} => ${value.requestUri}');
      }
      return expired;
    });
    _logger.finest('Cleaned up request cache has ${_responses.length} entries');
  }

  Future<http.Response> getRequestOrExecute({
    required Uri url,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
    Duration? timeout,
  }) async {
    _cleanupCache();
    const type = _RequestType.get;
    final requestStub = _CachedResponse.requestStub(
      type,
      url,
      headers,
      null,
    );

    late final http.Response response;
    if (_responses.containsKey(requestStub)) {
      print('Returning cached response for $type => $url');
      response = _responses[requestStub]?.response ?? http.Response('', 555);
    } else {
      final request = http.get(
        url,
        headers: headers,
      );

      response = await request.timeout(
        timeout ?? apiCallTimeout,
        onTimeout: requestTimeout,
      );

      final cacheEntry = _CachedResponse.response(
        type,
        url,
        headers ?? {},
        body ?? {},
        response,
      );

      print('Adding cached response for $type => $url');
      _responses[cacheEntry] = cacheEntry;
    }

    return response;
  }
}

final _cache = _ExpiringRequestCache();

FutureResult<PagedResponse<String>, ExecError> getUrl(
  Uri url, {
  Map<String, String>? headers,
  Duration? timeout,
}) async {
  _logger.fine('GET: $url');
  final requestHeaders = headers ?? {};
  if (usePhpDebugging) {
    requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
  }
  try {
    final response = await _cache.getRequestOrExecute(
      url: url,
      headers: headers,
      timeout: timeout ?? apiCallTimeout,
    );

    if (response.statusCode != 200) {
      return Result.error(ExecError(
          type: ErrorType.authentication,
          message: '${response.statusCode}: ${response.reasonPhrase}'));
    }
    return PagedResponse.fromLinkHeader(
      response.headers['link'],
      utf8.decode(response.bodyBytes),
    );
  } catch (e) {
    return Result.error(
        ExecError(type: ErrorType.localError, message: e.toString()));
  }
}

FutureResult<String, ExecError> postUrl(
  Uri url,
  Map<String, dynamic> body, {
  Map<String, String>? headers,
  Duration? timeout,
}) async {
  _logger.fine('POST: $url \n Body: $body');
  final requestHeaders = headers ?? {};
  if (usePhpDebugging) {
    requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
  }
  try {
    final request = http.post(
      url,
      headers: requestHeaders,
      body: jsonEncode(body),
    );

    final response = await request.timeout(
      timeout ?? apiCallTimeout,
      onTimeout: requestTimeout,
    );

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

FutureResult<String, ExecError> putUrl(
  Uri url,
  Map<String, dynamic> body, {
  Map<String, String>? headers,
  Duration? timeout,
}) async {
  _logger.fine('PUT: $url \n Body: $body');
  try {
    final request = http.put(
      url,
      headers: headers,
      body: jsonEncode(body),
    );

    final response = await request.timeout(
      timeout ?? apiCallTimeout,
      onTimeout: requestTimeout,
    );

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

FutureResult<String, ExecError> deleteUrl(
  Uri url,
  Map<String, dynamic> body, {
  Map<String, String>? headers,
  Duration? timeout,
}) async {
  _logger.fine('DELETE: $url');
  try {
    final request = http.delete(
      url,
      headers: headers,
      body: jsonEncode(body),
    );

    final response = await request.timeout(
      timeout ?? apiCallTimeout,
      onTimeout: requestTimeout,
    );

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

Uri generateTagUrlFromProfile(Profile profile, String tag) {
  return Uri.https(profile.serverName, '/search', {'tag': tag});
}
