// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/refresh_token_auth_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';

enum EmailVerificationAction {
  signUp,
  resetPassword,
  changeEmail;

  String toSnakeCase() => name.splitMapJoin(
      RegExp('[A-Z]'),
      onMatch: (m) => '_${m[0]?.toLowerCase()}',
      onNonMatch: (n) => n,
    );

  factory EmailVerificationAction.fromString(String str) => EmailVerificationAction.values.firstWhere(
      (element) => element.toSnakeCase() == str,
    );
}

class VerifyCode {
  final String email;
  final String verificationCode;
  final EmailVerificationAction action;
  VerifyCode({
    required this.email,
    required this.verificationCode,
    required this.action,
  });

  VerifyCode copyWith({
    String? email,
    String? verificationCode,
    EmailVerificationAction? action,
  }) => VerifyCode(
      email: email ?? this.email,
      verificationCode: verificationCode ?? this.verificationCode,
      action: action ?? this.action,
    );

  Map<String, dynamic> toMap() => <String, dynamic>{
      'email': email,
      'verification_code': verificationCode,
      'action': action.toSnakeCase(),
    };

  factory VerifyCode.fromMap(Map<String, dynamic> map) => VerifyCode(
      email: map['email'] as String,
      verificationCode: map['verification_code'] as String,
      action: EmailVerificationAction.fromString(map['action'] as String),
    );

  String toJson() => json.encode(toMap());

  factory VerifyCode.fromJson(String source) =>
      VerifyCode.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() =>
      'VerifyCode(email: $email, verificationCode: $verificationCode, action: $action)';
}

class Account {
  final String email;
  final String? sessionId;
  Account({
    required this.email,
    this.sessionId,
  });

  Account copyWith({
    String? email,
    String? sessionId,
  }) => Account(
      email: email ?? this.email,
      sessionId: sessionId ?? this.sessionId,
    );

  Map<String, dynamic> toMap() => <String, dynamic>{
      'email': email,
      'session_id': sessionId,
    };

  factory Account.fromMap(Map<String, dynamic> map) => Account(
      email: map['email'] as String,
      sessionId: map['session_id'] != null ? map['session_id'] as String : null,
    );

  String toJson() => json.encode(toMap());

  factory Account.fromJson(String source) =>
      Account.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() => 'Account(email: $email, sessionId: $sessionId)';
}

class SignUp {
  final String sessionId;
  final String password;
  SignUp({
    required this.sessionId,
    required this.password,
  });

  SignUp copyWith({
    String? sessionId,
    String? password,
  }) => SignUp(
      sessionId: sessionId ?? this.sessionId,
      password: password ?? this.password,
    );

  Map<String, dynamic> toMap() => <String, dynamic>{
      'session_id': sessionId,
      'password': password,
    };

  factory SignUp.fromMap(Map<String, dynamic> map) => SignUp(
      sessionId: map['session_id'] as String,
      password: map['password'] as String,
    );

  String toJson() => json.encode(toMap());

  factory SignUp.fromJson(String source) =>
      SignUp.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() => 'SignUp(sessionId: $sessionId, password: $password)';
}

class TokenSuccess {
  final String accessToken;
  final String refreshToken;
  final Account account;
  TokenSuccess({
    required this.accessToken,
    required this.refreshToken,
    required this.account,
  });

  TokenSuccess copyWith({
    String? accessToken,
    String? refreshToken,
    Account? account,
  }) => TokenSuccess(
      accessToken: accessToken ?? this.accessToken,
      refreshToken: refreshToken ?? this.refreshToken,
      account: account ?? this.account,
    );

  Map<String, dynamic> toMap() => <String, dynamic>{
      'access_token': accessToken,
      'refresh_token': refreshToken,
      'account': account.toMap(),
    };

  factory TokenSuccess.fromMap(Map<String, dynamic> map) => TokenSuccess(
      accessToken: map['access_token'] as String,
      refreshToken: map['refresh_token'] as String,
      account: Account.fromMap(map['account'] as Map<String, dynamic>),
    );

  String toJson() => json.encode(toMap());

  factory TokenSuccess.fromJson(String source) =>
      TokenSuccess.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() =>
      'TokenSuccess(accessToken: $accessToken, refreshToken: $refreshToken, account: $account)';
}

class Login {
  final String email;
  final String password;
  Login({
    required this.email,
    required this.password,
  });

  Login copyWith({
    String? email,
    String? password,
  }) => Login(
      email: email ?? this.email,
      password: password ?? this.password,
    );

  Map<String, dynamic> toMap() => <String, dynamic>{
      'email': email,
      'password': password,
    };

  factory Login.fromMap(Map<String, dynamic> map) => Login(
      email: map['email'] as String,
      password: map['password'] as String,
    );

  String toJson() => json.encode(toMap());

  factory Login.fromJson(String source) =>
      Login.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() => 'Login(email: $email, password: $password)';
}

class FastAPI {
  final String baseUrl;
  final MiddlewareClient client;
  final int apiVersion;

  FastAPI({
    this.baseUrl = 'localhost:80',
    MiddlewareClient? client,
    this.apiVersion = 1,
  }) : client = client ?? MiddlewareClient();

  String get apiPath => '/api/v$apiVersion';

  Future<void> sendSignUpCode(String email) async {
    final r = await client.post(
      Uri.parse('$apiPath/auth/send-sign-up-code'),
      body: <String, String>{
        'email': email,
      },
    );
    if (r.statusCode != 201) {
      throw Exception('Invalid reponse: ${r.statusCode}');
    }
  }

  Future<Account> verifyCode(VerifyCode verifyCode) async {
    final r = await client.post(
      Uri.parse('$apiPath/auth/verify-code'),
      body: verifyCode.toMap(),
    );
    if (r.statusCode != 202) {
      throw Exception('Invalid reponse: ${r.statusCode}');
    } else {
      return Account.fromMap(
        (jsonDecode(r.body) as Map<String, dynamic>)['account']
            as Map<String, dynamic>,
      );
    }
  }

  Future<Account> signUp(SignUp signUp) async {
    final r = await client.post(
      Uri.parse('$apiPath/auth/sign-up'),
      body: signUp.toMap(),
    );
    if (r.statusCode != 201) {
      throw Exception('Invalid reponse: ${r.statusCode}');
    } else {
      return Account.fromJson(r.body);
    }
  }

  Future<TokenSuccess> signInWithPassword(Login login) async {
    final r = await client.post(
      Uri.parse('$apiPath/auth/sign-in-with-password'),
      body: login.toMap(),
    );
    if (r.statusCode != 200) {
      throw Exception('Invalid reponse: ${r.statusCode}');
    } else {
      return TokenSuccess.fromJson(r.body);
    }
  }

  // Future<TokenSuccess> refresh() async {
  //   final r = await client.refresh();
  //   return TokenSuccess.fromJson(r?.body ?? '');
  // }

  Future<List<Account>> getAccountList() async {
    final r = await client.get(
      Uri.parse('$apiPath/account'),
    );
    if (r.statusCode != 200) {
      throw Exception('Invalid reponse: ${r.statusCode}');
    } else {
      final list = (jsonDecode(r.body) as Map<String, dynamic>)['founds']
          as List<Map<String, dynamic>>;
      final result = <Account>[];
      for (final element in list) {
        result.add(Account.fromMap(element));
      }
      return result;
    }
  }
}

void main(List<String> args) async {
  final Pipeline pipeline = Pipeline()
      .addMiddleware(
        UriPrefixMiddleware(
          protocol: Protocols.http,
          authority: 'localhost:80',
        ),
      )
      .addMiddleware(BodyToJsonMiddleware())
      .addMiddleware(
        RefreshTokenAuthMiddleware(
          authorizationEndpoint: '/api/v1/auth/sign-in-with-password',
          tokenEndpoint: '/api/v1/auth/refresh',
          accessTokenParser: (body) => body['access_token']! as String,
          refreshTokenParser: (body) => body['refresh_token']! as String,
          unauthorized: HttpStatus.forbidden,
        ),
      )
      .addMiddleware(SimpleLoggerMiddleware());

  print(pipeline);
  final client = MiddlewareClient(pipeline: pipeline);

  final api = FastAPI(
    client: client,
  );

  await api.sendSignUpCode('git@pcl.ovh');
  final verifiedAccount = await api.verifyCode(
    VerifyCode(
      email: 'git@pcl.ovh',
      verificationCode: '000000000',
      action: EmailVerificationAction.signUp,
    ),
  );
  final registeredAccount = await api.signUp(
    SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'),
  );
  final signedInAccount = await api.signInWithPassword(
    Login(email: 'git@pcl.ovh', password: 'password'),
  );
  final accountList = await api.getAccountList();
  print(accountList);
}
