import 'package:dio/dio.dart';

/// API 호출 실패 유형.
enum ApiErrorCode {
  badRequest,
  unauthorized,
  forbidden,
  notFound,
  conflict,
  unprocessableEntity,
  network,
  timeout,
  cancel,
  unknown,
}

/// API 호출 시 공통으로 던지는 예외 모델.
class ApiException implements Exception {
  const ApiException({
    required this.code,
    required this.message,
    this.statusCode,
    this.details,
    this.cause,
  });

  final ApiErrorCode code;
  final String message;
  final int? statusCode;
  final Map<String, dynamic>? details;
  final DioException? cause;

  @override
  String toString() =>
      'ApiException(code: $code, statusCode: $statusCode, message: $message)';
}

/// Dio 예외를 [ApiException]으로 변환하는 매퍼.
class ApiErrorMapper {
  const ApiErrorMapper();

  /// Dio 예외 세부 정보를 분석해 적절한 [ApiException]을 생성한다.
  ApiException map(DioException error) {
    final status = error.response?.statusCode;
    final data = error.response?.data;
    final message = _resolveMessage(error, data);
    final details = _extractDetails(data);

    if (error.type == DioExceptionType.connectionTimeout ||
        error.type == DioExceptionType.receiveTimeout ||
        error.type == DioExceptionType.sendTimeout) {
      return ApiException(
        code: ApiErrorCode.timeout,
        message: '서버 응답 시간이 초과되었습니다. 네트워크 상태를 확인하세요.',
        statusCode: status,
        cause: error,
      );
    }

    if (error.type == DioExceptionType.connectionError ||
        error.type == DioExceptionType.badCertificate) {
      return ApiException(
        code: ApiErrorCode.network,
        message: '네트워크 연결에 실패했습니다. 잠시 후 다시 시도하세요.',
        statusCode: status,
        cause: error,
      );
    }

    if (error.type == DioExceptionType.cancel) {
      return ApiException(
        code: ApiErrorCode.cancel,
        message: '요청이 취소되었습니다.',
        statusCode: status,
        cause: error,
      );
    }

    if (status != null) {
      switch (status) {
        case 400:
          return ApiException(
            code: ApiErrorCode.badRequest,
            message: message,
            statusCode: status,
            details: details,
            cause: error,
          );
        case 401:
          return ApiException(
            code: ApiErrorCode.unauthorized,
            message: _localizeUnauthorizedMessage(message),
            statusCode: status,
            details: details,
            cause: error,
          );
        case 403:
          return ApiException(
            code: ApiErrorCode.forbidden,
            message: message,
            statusCode: status,
            details: details,
            cause: error,
          );
        case 404:
          return ApiException(
            code: ApiErrorCode.notFound,
            message: '요청한 리소스를 찾을 수 없습니다.',
            statusCode: status,
            details: details,
            cause: error,
          );
        case 409:
          return ApiException(
            code: ApiErrorCode.conflict,
            message: message,
            statusCode: status,
            details: details,
            cause: error,
          );
        case 422:
          return ApiException(
            code: ApiErrorCode.unprocessableEntity,
            message: message,
            statusCode: status,
            details: details,
            cause: error,
          );
        default:
          break;
      }
    }

    return ApiException(
      code: ApiErrorCode.unknown,
      message: message,
      statusCode: status,
      details: details,
      cause: error,
    );
  }

  /// 응답 바디 혹은 Dio 예외 객체에서 사용자용 메시지를 추출한다.
  String _resolveMessage(DioException error, dynamic data) {
    if (data is Map<String, dynamic>) {
      final candidates = [
        data['message'],
        data['error_message'],
        data['errorMessage'],
        data['detail'],
      ];
      for (final candidate in candidates) {
        if (candidate is String && candidate.trim().isNotEmpty) {
          return candidate.trim();
        }
      }
      final errorNode = data['error'];
      if (errorNode is String && errorNode.trim().isNotEmpty) {
        return errorNode.trim();
      }
      if (errorNode is Map<String, dynamic>) {
        final nested = [
          errorNode['message'],
          errorNode['title'],
          errorNode['detail'],
          errorNode['error'],
        ];
        for (final candidate in nested) {
          if (candidate is String && candidate.trim().isNotEmpty) {
            return candidate.trim();
          }
        }
      }
      final messagesNode = data['messages'];
      if (messagesNode is Iterable) {
        final flattened = _flattenMessages(messagesNode);
        if (flattened != null) {
          return flattened;
        }
      }
      final errorsNode = data['errors'];
      final flattenedErrors = _flattenMessages(errorsNode);
      if (flattenedErrors != null) {
        return flattenedErrors;
      }
    } else if (data is String && data.trim().isNotEmpty) {
      return data.trim();
    }
    return error.message?.trim() ?? '요청 처리 중 알 수 없는 오류가 발생했습니다.';
  }

  /// 422/409 등에서 제공되는 필드별 오류 정보를 추출한다.
  Map<String, dynamic>? _extractDetails(dynamic data) {
    if (data is! Map) {
      return null;
    }
    Map<String, dynamic>? result;

    void merge(dynamic node) {
      if (node == null) {
        return;
      }
      if (node is Map) {
        if (node.isEmpty) {
          return;
        }
        result ??= <String, dynamic>{};
        node.forEach((rawKey, rawValue) {
          final key = rawKey?.toString() ?? '';
          if (key.isEmpty) {
            return;
          }
          final values = _asList(rawValue);
          if (values.isEmpty) {
            return;
          }
          final existing = result![key];
          if (existing == null) {
            result![key] = values.length == 1 ? values.first : values;
            return;
          }
          final merged = _asList(existing)..addAll(values);
          result![key] = merged.length == 1 ? merged.first : merged;
        });
        return;
      }
      if (node is Iterable) {
        final mapped = _mapFromErrors(node);
        if (mapped != null) {
          merge(mapped);
        }
        return;
      }
      final mapped = _mapFromErrors([node]);
      if (mapped != null) {
        merge(mapped);
      }
    }

    final map = data.cast<dynamic, dynamic>();
    merge(map['errors']);
    merge(map['details']);
    merge(map['context']);

    final errorNode = map['error'];
    if (errorNode is Map) {
      merge(errorNode['errors']);
      merge(errorNode['details']);
      merge(errorNode['context']);
    }

    return result;
  }

  String? _flattenMessages(dynamic source) {
    if (source == null) {
      return null;
    }
    if (source is String) {
      final trimmed = source.trim();
      return trimmed.isEmpty ? null : trimmed;
    }
    if (source is Iterable) {
      final buffer = <String>[];
      for (final item in source) {
        final value = _flattenMessages(item);
        if (value != null && value.isNotEmpty) {
          buffer.add(value);
        }
      }
      if (buffer.isEmpty) {
        return null;
      }
      return buffer.join('\n');
    }
    if (source is Map) {
      final message =
          source['message'] ??
          source['detail'] ??
          source['error'] ??
          source['description'];
      final flattened = _flattenMessages(message);
      if (flattened != null) {
        return flattened;
      }
      return _flattenMessages(source.values);
    }
    if (source is num || source is bool) {
      return source.toString();
    }
    return source.toString();
  }

  Map<String, dynamic>? _mapFromErrors(Iterable<dynamic> errors) {
    final result = <String, dynamic>{};
    for (final entry in errors) {
      if (entry is Map) {
        final field = entry['field'] ?? entry['name'] ?? entry['key'];
        final messages =
            entry['messages'] ??
            entry['message'] ??
            entry['detail'] ??
            entry['error'];
        if (field is String) {
          final bucket = result[field];
          final nextMessages = _asList(messages);
          if (bucket is List) {
            bucket.addAll(nextMessages);
          } else if (bucket != null) {
            result[field] = _asList(bucket)..addAll(nextMessages);
          } else if (nextMessages.length == 1) {
            result[field] = nextMessages.first;
          } else {
            result[field] = nextMessages;
          }
          continue;
        }
      }
      final list =
          result.putIfAbsent('general', () => <dynamic>[]) as List<dynamic>;
      list.add(entry);
    }
    return result.isEmpty ? null : result;
  }

  List<dynamic> _asList(dynamic value) {
    if (value == null) {
      return <dynamic>[];
    }
    if (value is List) {
      return value;
    }
    if (value is Iterable) {
      return value.toList();
    }
    return <dynamic>[value];
  }

  /// 인증 실패 응답 메시지를 한글 안내로 정규화한다.
  String _localizeUnauthorizedMessage(String message) {
    final trimmed = message.trim();
    if (trimmed.isEmpty) {
      return '세션이 만료되었습니다. 다시 로그인해 주세요.';
    }
    final normalized = trimmed.toLowerCase();
    switch (normalized) {
      case 'invalid credentials':
        return '아이디 또는 비밀번호가 올바르지 않습니다.';
      case 'account is inactive':
        return '비활성 계정입니다. 관리자에게 문의하세요.';
      case 'token expired':
        return '세션이 만료되었습니다. 다시 로그인해 주세요.';
      case 'invalid token':
        return '유효하지 않은 토큰입니다. 다시 로그인해 주세요.';
      default:
        return trimmed;
    }
  }
}
