import 'dart:convert';

import 'package:shelf/shelf.dart';

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

final class SyncApi {
  const SyncApi({
    required this.pushSync,
    required this.pullSync,
    required this.exchangeSync,
    required this.authenticateRequest,
  });

  final PushSyncUseCase pushSync;
  final PullSyncUseCase pullSync;
  final ExchangeSyncUseCase exchangeSync;
  final AuthenticateRequestUseCase authenticateRequest;

  Future<Response> push(Request request) async {
    final authenticated = authenticatedRequestFrom(request);
    if (authenticated == null) {
      return _errorResponse(401, 'Unauthorized.');
    }
    try {
      final body = await _readJsonObject(request);
      final result = await pushSync.execute(
        ownerUserId: authenticated.user.id,
        deviceId: _optionalString(body, 'deviceId'),
        items: _pushItems(body['items']),
      );
      return _jsonResponse(200, _pushResultJson(result));
    } on FormatException catch (error) {
      return _errorResponse(400, error.message);
    }
  }

  Future<Response> pull(Request request) async {
    final authenticated = authenticatedRequestFrom(request);
    if (authenticated == null) {
      return _errorResponse(401, 'Unauthorized.');
    }
    try {
      final since = _optionalDateTime(request.url.queryParameters['since']);
      final result = await pullSync.execute(
        ownerUserId: authenticated.user.id,
        since: since,
      );
      return _jsonResponse(200, _pullResultJson(result));
    } on FormatException catch (error) {
      return _errorResponse(400, error.message);
    }
  }

  Future<Response> exchange(Request request) async {
    final authenticated = authenticatedRequestFrom(request);
    if (authenticated == null) {
      return _errorResponse(401, 'Unauthorized.');
    }
    try {
      final body = await _readJsonObject(request);
      final result = await exchangeSync.execute(
        ownerUserId: authenticated.user.id,
        deviceId: _optionalString(body, 'deviceId'),
        items: _pushItems(body['items']),
        since: _optionalDateTime(_optionalString(body, 'since')),
      );
      return _jsonResponse(200, {
        'serverCursor': result.pull.serverCursor.toIso8601String(),
        'pushResults': _pushResultsJson(result.push.results),
        'items': _resourcesJson(result.pull.items),
      });
    } on FormatException catch (error) {
      return _errorResponse(400, error.message);
    }
  }
}

List<PushSyncItemInput> _pushItems(Object? rawItems) {
  if (rawItems is! List) {
    throw const FormatException('items must be a JSON array.');
  }
  return [
    for (final rawItem in rawItems)
      if (rawItem is Map)
        _pushItem(Map<String, Object?>.from(rawItem))
      else
        const PushSyncItemInput.invalid(
          message: 'Sync item must be a JSON object.',
        ),
  ];
}

PushSyncItemInput _pushItem(Map<String, Object?> item) {
  try {
    final payload = item['payload'];
    if (payload is! Map) {
      return PushSyncItemInput.invalid(
        resourceType: _stringOrNull(item['resourceType']),
        clientId: _stringOrNull(item['clientId']),
        message: 'payload must be a JSON object.',
      );
    }
    return PushSyncItemInput(
      resourceType: _stringOrNull(item['resourceType']),
      clientId: _stringOrNull(item['clientId']),
      schemaVersion: item['schemaVersion'] is int
          ? item['schemaVersion'] as int
          : null,
      clientUpdatedAt: _optionalDateTime(
        _stringOrNull(item['clientUpdatedAt']),
      ),
      deletedAt: _optionalDateTime(_stringOrNull(item['deletedAt'])),
      payloadJson: Map<String, Object?>.from(payload),
    );
  } on FormatException catch (error) {
    return PushSyncItemInput.invalid(
      resourceType: _stringOrNull(item['resourceType']),
      clientId: _stringOrNull(item['clientId']),
      message: error.message,
    );
  }
}

Map<String, Object?> _pushResultJson(PushSyncResult result) {
  return {
    'serverCursor': result.serverCursor.toIso8601String(),
    'results': _pushResultsJson(result.results),
  };
}

List<Map<String, Object?>> _pushResultsJson(List<PushSyncResultItem> results) {
  return [
    for (final item in results)
      {
        'resourceType': item.resourceType,
        'clientId': item.clientId,
        'serverId': item.serverId,
        'status': item.status,
        'serverUpdatedAt': item.serverUpdatedAt?.toIso8601String(),
        if (item.message != null) 'message': item.message,
      },
  ];
}

Map<String, Object?> _pullResultJson(PullSyncResult result) {
  return {
    'serverCursor': result.serverCursor.toIso8601String(),
    'items': _resourcesJson(result.items),
  };
}

List<Map<String, Object?>> _resourcesJson(List<SyncedResource> resources) {
  return [
    for (final resource in resources)
      {
        'resourceType': resource.resourceType.wireName,
        'clientId': resource.clientId,
        'serverId': resource.serverId,
        'schemaVersion': resource.schemaVersion,
        'clientUpdatedAt': resource.clientUpdatedAt.toIso8601String(),
        'serverUpdatedAt': resource.serverUpdatedAt.toIso8601String(),
        'deletedAt': resource.deletedAt?.toIso8601String(),
        'payload': resource.payloadJson,
      },
  ];
}

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? _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;
}

String? _stringOrNull(Object? value) {
  if (value == null) {
    return null;
  }
  return value is String ? value : null;
}

DateTime? _optionalDateTime(String? value) {
  if (value == null) {
    return null;
  }
  return DateTime.parse(value).toUtc();
}

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});
}
