import 'package:dio/dio.dart';

import 'api_error.dart';

/// 서버/네트워크 예외를 사용자 메시지와 필드 오류 형태로 정규화한 객체.
class Failure {
  const Failure({
    required this.message,
    this.code,
    this.statusCode,
    this.reasons = const [],
    this.fieldErrors = const {},
    this.raw,
  });

  /// 사용자에게 우선 노출할 기본 메시지.
  final String message;

  /// [ApiErrorCode] 또는 유사 코드.
  final ApiErrorCode? code;

  /// HTTP 상태 코드(존재하는 경우).
  final int? statusCode;

  /// 추가 설명 메시지 목록.
  final List<String> reasons;

  /// 필드별 오류 메시지 묶음.
  final Map<String, List<String>> fieldErrors;

  /// 원본 예외 객체.
  final Object? raw;

  /// 필드 오류가 존재하는지 여부.
  bool get hasFieldErrors => fieldErrors.isNotEmpty;

  /// 추가 설명이 존재하는지 여부.
  bool get hasReasons => reasons.isNotEmpty;

  /// 필드명에 해당하는 첫 번째 오류 메시지를 반환한다.
  String? firstFieldError(String field) {
    final messages = fieldErrors[field];
    if (messages == null) {
      return null;
    }
    for (final message in messages) {
      final trimmed = message.trim();
      if (trimmed.isNotEmpty) {
        return trimmed;
      }
    }
    return null;
  }

  /// 필드 오류를 줄바꿈으로 합친 문자열 맵을 반환한다.
  Map<String, String> get fieldErrorMessages {
    final result = <String, String>{};
    fieldErrors.forEach((key, value) {
      final messages = _dedupeStrings(value);
      if (messages.isEmpty) {
        return;
      }
      result[key] = messages.join('\n');
    });
    return result;
  }

  /// 기본 메시지와 추가 설명 및 필드 오류를 단일 문자열로 합친다.
  String describe({String separator = '\n', bool includeFieldErrors = true}) {
    final lines = <String>[];
    final base = message.trim();
    if (base.isNotEmpty) {
      lines.add(base);
    }
    for (final reason in reasons) {
      final trimmed = reason.trim();
      if (trimmed.isEmpty) {
        continue;
      }
      final duplicate = lines.any(
        (existing) => existing.toLowerCase() == trimmed.toLowerCase(),
      );
      if (!duplicate) {
        lines.add(trimmed);
      }
    }
    if (includeFieldErrors && fieldErrors.isNotEmpty) {
      fieldErrors.forEach((key, value) {
        final first = value.firstWhere(
          (message) => message.trim().isNotEmpty,
          orElse: () => '',
        );
        final trimmed = first.trim();
        if (trimmed.isEmpty) {
          return;
        }
        final line = '$key: $trimmed';
        final duplicate = lines.any(
          (existing) => existing.toLowerCase() == line.toLowerCase(),
        );
        if (!duplicate) {
          lines.add(line);
        }
      });
    }
    return lines.join(separator).trim();
  }

  /// 임의 객체에서 [Failure]를 생성한다.
  static Failure from(Object error) => const FailureParser().parse(error);
}

/// [Failure] 변환을 담당하는 파서.
class FailureParser {
  const FailureParser();

  Failure parse(Object error) {
    if (error is Failure) {
      return error;
    }
    if (error is ApiException) {
      return _fromApiException(error);
    }
    if (error is DioException) {
      final mapped = const ApiErrorMapper().map(error);
      return _fromApiException(mapped);
    }
    if (error is Exception || error is Error) {
      final message = _exceptionMessage(error);
      return Failure(message: message, raw: error);
    }
    final fallback = error.toString().trim();
    return Failure(
      message: fallback.isEmpty ? '요청 처리 중 오류가 발생했습니다.' : fallback,
      raw: error,
    );
  }

  Failure _fromApiException(ApiException exception) {
    final payload = _FailurePayload();
    payload.consumeDetails(exception.details);
    payload.consumeResponseData(exception.cause?.response?.data);

    final message =
        _firstNonEmpty([payload.message, exception.message]) ??
        '요청 처리 중 오류가 발생했습니다.';

    final reasons = payload.reasons
        .where(
          (reason) =>
              reason.trim().toLowerCase() != message.trim().toLowerCase(),
        )
        .toList(growable: false);

    return Failure(
      message: message,
      code: exception.code,
      statusCode: exception.statusCode,
      reasons: reasons,
      fieldErrors: payload.fieldErrors.map(
        (key, value) => MapEntry(key, _dedupeStrings(value)),
      ),
      raw: exception,
    );
  }

  String _exceptionMessage(Object error) {
    final raw = error.toString();
    final trimmed = raw.trim();
    if (trimmed.startsWith('Exception: ')) {
      final stripped = trimmed.substring('Exception: '.length).trim();
      if (stripped.isNotEmpty) {
        return stripped;
      }
    }
    if (trimmed.startsWith('Error: ')) {
      final stripped = trimmed.substring('Error: '.length).trim();
      if (stripped.isNotEmpty) {
        return stripped;
      }
    }
    return trimmed.isEmpty ? '요청 처리 중 오류가 발생했습니다.' : trimmed;
  }
}

class _FailurePayload {
  String? message;
  final List<String> reasons = [];
  final Map<String, List<String>> fieldErrors = {};

  void consumeDetails(Map<String, dynamic>? details) {
    if (details == null || details.isEmpty) {
      return;
    }
    details.forEach((rawKey, rawValue) {
      final key = rawKey.toString();
      final values = _flattenMessages(rawValue);
      if (values.isEmpty) {
        return;
      }
      if (_isGeneralKey(key)) {
        reasons.addAll(values);
      } else {
        fieldErrors.putIfAbsent(key, () => <String>[]).addAll(values);
      }
    });
  }

  void consumeResponseData(dynamic data) {
    if (data == null) {
      return;
    }
    if (data is Map<String, dynamic>) {
      _consumeMap(data);
      return;
    }
    if (data is Iterable) {
      reasons.addAll(_flattenIterable(data));
      return;
    }
    final text = _toMessage(data);
    if (text != null) {
      reasons.add(text);
    }
  }

  void _consumeMap(Map<String, dynamic> map) {
    final messageCandidate = _firstNonEmpty([
      _toMessage(map['message']),
      _toMessage(map['error_message']),
      _toMessage(map['errorMessage']),
      _toMessage(map['detail']),
    ]);
    if (messageCandidate != null) {
      message = messageCandidate;
    }

    _consumeErrorsNode(map['errors']);
    _consumeErrorsNode(map['details']);
    _consumeErrorsNode(map['reason']);
    _consumeErrorsNode(map['reasons']);
    _consumeErrorsNode(map['context']);

    final errorNode = map['error'];
    if (errorNode is Map<String, dynamic>) {
      final nodeMessage = _firstNonEmpty([
        _toMessage(errorNode['message']),
        _toMessage(errorNode['title']),
        _toMessage(errorNode['detail']),
      ]);
      message ??= nodeMessage;
      _consumeErrorsNode(errorNode['errors']);
      _consumeErrorsNode(errorNode['details']);
      _consumeErrorsNode(errorNode['reason']);
      _consumeErrorsNode(errorNode['reasons']);
      _consumeErrorsNode(errorNode['context']);
      final extra = _flattenIterable(_ensureIterable(errorNode['messages']));
      reasons.addAll(extra);
    } else if (errorNode != null) {
      final text = _toMessage(errorNode);
      if (text != null) {
        reasons.add(text);
      }
    }

    final messagesNode = map['messages'];
    reasons.addAll(_flattenIterable(_ensureIterable(messagesNode)));
  }

  void _consumeErrorsNode(dynamic node) {
    if (node == null) {
      return;
    }
    if (node is Map<String, dynamic>) {
      node.forEach((rawKey, rawValue) {
        final key = rawKey.toString();
        final values = _flattenMessages(rawValue);
        if (values.isEmpty) {
          return;
        }
        if (_isGeneralKey(key)) {
          reasons.addAll(values);
        } else {
          fieldErrors.putIfAbsent(key, () => <String>[]).addAll(values);
        }
      });
      return;
    }
    if (node is Iterable) {
      for (final item in node) {
        if (item is Map<String, dynamic>) {
          final field = _firstNonEmpty([
            _toMessage(item['field']),
            _toMessage(item['name']),
            _toMessage(item['key']),
          ]);
          final messages = _flattenMessages(
            item['messages'] ??
                item['message'] ??
                item['detail'] ??
                item['error'],
          );
          if (field != null && field.trim().isNotEmpty) {
            if (messages.isEmpty) {
              final fallback = _flattenMessages(item);
              fieldErrors.putIfAbsent(field, () => <String>[]).addAll(fallback);
            } else {
              fieldErrors.putIfAbsent(field, () => <String>[]).addAll(messages);
            }
          } else if (messages.isNotEmpty) {
            reasons.addAll(messages);
          } else {
            reasons.addAll(_flattenMessages(item.values));
          }
        } else {
          reasons.addAll(_flattenMessages(item));
        }
      }
      return;
    }
    final values = _flattenMessages(node);
    if (values.isNotEmpty) {
      reasons.addAll(values);
    }
  }
}

const Set<String> _generalErrorKeys = {
  'general',
  'base',
  'common',
  'global',
  '_global',
  '_common',
  '_general',
  'summary',
  'message',
  'messages',
  'detail',
  'details',
  'reason',
  'reasons',
  'non_field_errors',
  'nonfielderrors',
};

bool _isGeneralKey(String key) {
  final normalized = key.trim().toLowerCase();
  return _generalErrorKeys.contains(normalized);
}

Iterable<dynamic> _ensureIterable(dynamic value) {
  if (value is Iterable) {
    return value;
  }
  if (value == null) {
    return const [];
  }
  return [value];
}

String? _firstNonEmpty(Iterable<String?> candidates) {
  for (final candidate in candidates) {
    final text = candidate?.trim();
    if (text != null && text.isNotEmpty) {
      return text;
    }
  }
  return null;
}

String? _toMessage(dynamic value) {
  if (value == null) {
    return null;
  }
  if (value is String) {
    final trimmed = value.trim();
    return trimmed.isEmpty ? null : trimmed;
  }
  if (value is num || value is bool) {
    return value.toString();
  }
  return null;
}

List<String> _flattenIterable(Iterable<dynamic> values) {
  final aggregated = <String>[];
  for (final value in values) {
    aggregated.addAll(_flattenMessages(value));
  }
  return aggregated;
}

List<String> _flattenMessages(dynamic value) {
  if (value == null) {
    return const [];
  }
  if (value is String) {
    final trimmed = value.trim();
    return trimmed.isEmpty ? const [] : [trimmed];
  }
  if (value is num || value is bool) {
    return [value.toString()];
  }
  if (value is Iterable) {
    return _flattenIterable(value);
  }
  if (value is Map) {
    final map = value.cast<dynamic, dynamic>();
    final candidates = <String>[];
    final directMessage = _firstNonEmpty([
      _toMessage(map['message']),
      _toMessage(map['detail']),
      _toMessage(map['error']),
      _toMessage(map['description']),
      _toMessage(map['reason']),
      _toMessage(map['title']),
    ]);
    if (directMessage != null) {
      candidates.add(directMessage);
    }
    final messagesNode = map['messages'];
    if (messagesNode is Iterable) {
      candidates.addAll(_flattenIterable(messagesNode));
    }
    if (candidates.isNotEmpty) {
      return candidates;
    }
    return _flattenIterable(map.values);
  }
  return [_toMessage(value) ?? value.toString()];
}

List<String> _dedupeStrings(Iterable<String> values) {
  final result = <String>[];
  final seen = <String>{};
  for (final value in values) {
    final trimmed = value.trim();
    if (trimmed.isEmpty) {
      continue;
    }
    final normalized = trimmed.toLowerCase();
    if (seen.add(normalized)) {
      result.add(trimmed);
    }
  }
  return result;
}
