import 'dart:convert';

import 'package:shelf/shelf.dart';

import '../application/application.dart';
import '../domain/domain.dart';

const authenticatedRequestContextKey = 'gametime.authenticatedRequest';
const bearerTokenContextKey = 'gametime.bearerToken';

typedef AuthenticateToken =
    Future<AuthenticatedRequest> Function({required String token});

final class AuthApi {
  const AuthApi({
    required this.registerUser,
    required this.login,
    required this.logout,
    required this.authenticateRequest,
  });

  final RegisterUserUseCase registerUser;
  final LoginUseCase login;
  final LogoutUseCase logout;
  final AuthenticateRequestUseCase authenticateRequest;

  Future<Response> register(Request request) async {
    try {
      final body = await _readJsonObject(request);
      final user = await registerUser.execute(
        email: _requiredString(body, 'email'),
        password: _requiredString(body, 'password'),
        displayName: _optionalString(body, 'displayName'),
      );
      return _jsonResponse(201, {'userId': user.id, 'email': user.email});
    } on EmailAlreadyTakenException catch (error) {
      return _errorResponse(409, error.message);
    } on ValidationException catch (error) {
      return _errorResponse(400, error.message);
    } on FormatException catch (error) {
      return _errorResponse(400, error.message);
    }
  }

  Future<Response> loginUser(Request request) async {
    try {
      final body = await _readJsonObject(request);
      final result = await login.execute(
        email: _requiredString(body, 'email'),
        password: _requiredString(body, 'password'),
        userAgent: request.headers['user-agent'],
        deviceLabel: _optionalString(body, 'deviceLabel'),
      );
      return _jsonResponse(200, {
        'token': result.token,
        'expiresAt': result.expiresAt.toIso8601String(),
      });
    } on InvalidCredentialsException catch (error) {
      return _errorResponse(401, error.message);
    } on ValidationException catch (error) {
      return _errorResponse(400, error.message);
    } on FormatException catch (error) {
      return _errorResponse(400, error.message);
    }
  }

  Future<Response> logoutUser(Request request) async {
    final token = request.context[bearerTokenContextKey] as String?;
    if (token == null) {
      return _errorResponse(401, 'Unauthorized.');
    }
    try {
      await logout.execute(token: token);
      return Response(204);
    } on UnauthorizedException catch (error) {
      return _errorResponse(401, error.message);
    }
  }
}

Middleware authenticationMiddleware(AuthenticateToken authenticate) {
  return (innerHandler) {
    return (request) async {
      final token = _bearerToken(request.headers['authorization']);
      if (token == null) {
        return _errorResponse(401, 'Missing bearer token.');
      }

      try {
        final authenticated = await authenticate(token: token);
        return innerHandler(
          request.change(
            context: {
              authenticatedRequestContextKey: authenticated,
              bearerTokenContextKey: token,
            },
          ),
        );
      } on UnauthorizedException catch (error) {
        return _errorResponse(401, error.message);
      }
    };
  };
}

AuthenticatedRequest? authenticatedRequestFrom(Request request) {
  return request.context[authenticatedRequestContextKey]
      as AuthenticatedRequest?;
}

String? _bearerToken(String? authorization) {
  if (authorization == null) {
    return null;
  }
  final parts = authorization.trim().split(RegExp(r'\s+'));
  if (parts.length != 2 || parts.first.toLowerCase() != 'bearer') {
    return null;
  }
  return parts[1].isEmpty ? null : parts[1];
}

Future<Map<String, Object?>> _readJsonObject(Request request) async {
  final raw = await request.readAsString();
  final decoded = jsonDecode(raw);
  if (decoded is! Map<String, Object?>) {
    throw const FormatException('Request body must be a JSON object.');
  }
  return decoded;
}

String _requiredString(Map<String, Object?> body, String key) {
  final value = body[key];
  if (value is! String || value.trim().isEmpty) {
    throw FormatException('$key must be a non-empty string.');
  }
  return value;
}

String? _optionalString(Map<String, Object?> body, String key) {
  final value = body[key];
  if (value == null) {
    return null;
  }
  if (value is! String) {
    throw FormatException('$key must be a string.');
  }
  final trimmed = value.trim();
  return trimmed.isEmpty ? null : trimmed;
}

Response _jsonResponse(int statusCode, Map<String, Object?> body) {
  return Response(
    statusCode,
    body: jsonEncode(body),
    headers: {'content-type': 'application/json'},
  );
}

Response _errorResponse(int statusCode, String message) {
  return _jsonResponse(statusCode, {'error': message});
}
