import 'dart:convert' show jsonDecode, jsonEncode;

import 'package:fotodocumentation/dto/base_dto.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/http_client_utils.dart';
import 'package:fotodocumentation/utils/jwt_token_storage.dart';
import 'package:fotodocumentation/utils/url_utils.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart' show Response;

import 'package:fotodocumentation/main.dart' show logger;

abstract class BaseController {
  UrlUtils get uriUtils => DiContainer.get();
  JwtTokenStorage get _jwtTokenStorage => DiContainer.get();
  HttpClientUtils get httpClientUtils => DiContainer.get();

  Future<Header> getAuthHeader() async {
    final accessToken = await _jwtTokenStorage.getAccessToken();

    if (accessToken != null && accessToken.isNotEmpty) {
      // Use JWT Bearer token
      return Header('Authorization', 'Bearer $accessToken');
    } else {
      return const Header("Accept-Language", "en-US");
    }
  }

  Exception getServerError(Response response) {
    return Exception("Error receiving data from server");
  }

  Future<List<T>> runGetListWithAuth<T>(String uriStr, List<T> Function(dynamic) convert) async {
    http.Client client = httpClientUtils.client;
    try {
      Header cred = await getAuthHeader();
      Uri uri = Uri.parse(uriStr);
      var response = await client.get(uri, headers: {cred.name: cred.value});
      if (response.statusCode == 200) {
        String text = response.body;
        var jsonArray = jsonDecode(text);
        return convert(jsonArray);
      } else {
        throw ServerError(response.statusCode); // Exception("Failed to get server data ${response.statusCode}");
      }
    } catch (e) {
      logger.e("exception $e");
      rethrow;
    }
  }

  Future<T?> runGetWithAuth<T>(String uriStr, T Function(dynamic) convert) async {
    http.Client client = httpClientUtils.client;
    try {
      Header cred = await getAuthHeader();
      Uri uri = Uri.parse(uriStr);
      var response = await client.get(uri, headers: {cred.name: cred.value});
      if (response.statusCode == 200) {
        String text = response.body;
        var jsonArray = jsonDecode(text);
        return convert(jsonArray);
      } else {
        throw ServerError(response.statusCode); // Exception("Failed to get server data ${response.statusCode}");
      }
    } catch (e) {
      logger.e("exception $e");
      rethrow;
    }
  }

  Future<bool> runDeleteWithAuth(String uriStr) async {
    http.Client client = httpClientUtils.client;
    Header cred = await getAuthHeader();
    Uri uri = Uri.parse(uriStr);
    var response = await client.delete(uri, headers: {cred.name: cred.value});
    return response.statusCode == 200;
  }

  Future<bool> runPutWithAuth(String uriStr) async {
    http.Client client = httpClientUtils.client;
    Header cred = await getAuthHeader();
    Uri uri = Uri.parse(uriStr);
    var response = await client.put(uri, headers: {cred.name: cred.value});
    return response.statusCode == 200;
  }

  Future<ServerReply<T>> runSaveNew<T extends DtoMapAble>(String uriStr, T dtoObj, Function(http.Response response, T dto) processReply) async {
    http.Client client = httpClientUtils.client;
    try {
      Header cred = await getAuthHeader();
      String body = jsonEncode(dtoObj.toMap());

      Uri uri = Uri.parse(uriStr);
      var response = await client.post(uri, headers: {cred.name: cred.value, "Accept": "application/json", "Content-Type": "application/json"}, body: body);
      return processReply(response, dtoObj);
    } catch (e) {
      logger.e("exception $e");
    }
    return ServerReply(ServerState.error, dtoObj);
  }

  Future<ServerReply<T>> runSaveUpdate<T extends DtoMapAble>(String uriStr, T dtoObj, Function(http.Response response, T dto) processReply) async {
    http.Client client = httpClientUtils.client;
    try {
      Header cred = await getAuthHeader();
      String body = jsonEncode(dtoObj.toMap());

      Uri uri = Uri.parse(uriStr);
      var response = await client.put(uri, headers: {cred.name: cred.value, "Accept": "application/json", "Content-Type": "application/json"}, body: body);

      return processReply(response, dtoObj);
    } catch (e) {
      logger.e("exception $e");
    }
    return ServerReply(ServerState.error, dtoObj);
  }

  ServerReply<T> processServerResponse<T>(Response response, T dto, T Function(Map<String, dynamic> json) fromJson) {
    if (response.statusCode == 200) {
      String text = response.body;
      var json = jsonDecode(text);
      var dto = fromJson(json);
      return ServerReply(ServerState.ok, dto);
    } else if (response.statusCode == 400 || response.statusCode == 409) {
      String text = response.body;
      try {
        var json = jsonDecode(text);
        var error = ErrorDto.fromJson(json);

        return ServerReply(ServerState.duplicate, dto, error: error);
      } catch (e) {
        return ServerReply(ServerState.error, dto, error: ErrorDto(response.statusCode, text));
      }
    } else if (response.statusCode == 403) {
      var error = ErrorDto(403, "Not allowed.");
      return ServerReply(ServerState.error, dto, error: error);
    }
    return ServerReply(ServerState.error, dto, error: ErrorDto(response.statusCode, "Internal server error"));
  }
}

class Header {
  final String name;
  final String value;

  const Header(this.name, this.value);

  @override
  String toString() => '$runtimeType: $name, $value';
}

class ServerReply<T> {
  ServerState state;
  T entity;
  ErrorDto? error;

  ServerReply(this.state, this.entity, {this.error});

  @override
  String toString() => '$runtimeType: $state, $entity, $error';
}

class ServerError {
  int statusCode;

  ServerError(this.statusCode);
}

enum ServerState {
  ok,
  duplicate,
  error,
  ;
}
