// Models for the Local Intel Service (OpenMeteo + Overpass API data)

/// Weather conditions from OpenMeteo API
class WeatherConditions {
  final double temperature;
  final int weatherCode;
  final double uvIndex;
  final double windSpeed;
  final int humidity;
  final double feelsLike;
  final DateTime timestamp;

  WeatherConditions({
    required this.temperature,
    required this.weatherCode,
    required this.uvIndex,
    required this.windSpeed,
    required this.humidity,
    required this.feelsLike,
    required this.timestamp,
  });

  factory WeatherConditions.fromJson(Map<String, dynamic> json) {
    final current = json['current'] as Map<String, dynamic>? ?? {};
    return WeatherConditions(
      temperature: (current['temperature_2m'] as num?)?.toDouble() ?? 0.0,
      weatherCode: (current['weather_code'] as num?)?.toInt() ?? 0,
      uvIndex: (current['uv_index'] as num?)?.toDouble() ?? 0.0,
      windSpeed: (current['wind_speed_10m'] as num?)?.toDouble() ?? 0.0,
      humidity: (current['relative_humidity_2m'] as num?)?.toInt() ?? 0,
      feelsLike: (current['apparent_temperature'] as num?)?.toDouble() ?? 0.0,
      timestamp: DateTime.tryParse(current['time'] ?? '') ?? DateTime.now(),
    );
  }

  /// Weather code to human-readable description
  String get weatherDescription {
    switch (weatherCode) {
      case 0:
        return 'Clear sky';
      case 1:
        return 'Mainly clear';
      case 2:
        return 'Partly cloudy';
      case 3:
        return 'Overcast';
      case 45:
      case 48:
        return 'Foggy';
      case 51:
      case 53:
      case 55:
        return 'Drizzle';
      case 56:
      case 57:
        return 'Freezing drizzle';
      case 61:
      case 63:
      case 65:
        return 'Rain';
      case 66:
      case 67:
        return 'Freezing rain';
      case 71:
      case 73:
      case 75:
        return 'Snowfall';
      case 77:
        return 'Snow grains';
      case 80:
      case 81:
      case 82:
        return 'Rain showers';
      case 85:
      case 86:
        return 'Snow showers';
      case 95:
        return 'Thunderstorm';
      case 96:
      case 99:
        return 'Thunderstorm with hail';
      default:
        return 'Unknown';
    }
  }

  /// Weather code to icon name
  String get weatherIcon {
    switch (weatherCode) {
      case 0:
        return 'wb_sunny';
      case 1:
      case 2:
        return 'partly_cloudy_day';
      case 3:
        return 'cloud';
      case 45:
      case 48:
        return 'foggy';
      case 51:
      case 53:
      case 55:
      case 56:
      case 57:
      case 61:
      case 63:
      case 65:
      case 66:
      case 67:
      case 80:
      case 81:
      case 82:
        return 'rainy';
      case 71:
      case 73:
      case 75:
      case 77:
      case 85:
      case 86:
        return 'ac_unit';
      case 95:
      case 96:
      case 99:
        return 'thunderstorm';
      default:
        return 'cloud';
    }
  }

  /// UV Index risk level
  String get uvRiskLevel {
    if (uvIndex < 3) return 'Low';
    if (uvIndex < 6) return 'Moderate';
    if (uvIndex < 8) return 'High';
    if (uvIndex < 11) return 'Very High';
    return 'Extreme';
  }
}

/// Air quality and environmental hazards from OpenMeteo Air Quality API
class EnvironmentalHazards {
  final int aqi; // US EPA AQI
  final double pm25;
  final double pm10;
  final int grassPollen;
  final int birchPollen;
  final int olivePollen;
  final int ragweedPollen;
  final double uvIndex;
  final DateTime timestamp;

  EnvironmentalHazards({
    required this.aqi,
    required this.pm25,
    required this.pm10,
    required this.grassPollen,
    required this.birchPollen,
    required this.olivePollen,
    required this.ragweedPollen,
    required this.uvIndex,
    required this.timestamp,
  });

  factory EnvironmentalHazards.fromJson(Map<String, dynamic> json) {
    final current = json['current'] as Map<String, dynamic>? ?? {};
    return EnvironmentalHazards(
      aqi: (current['us_aqi'] as num?)?.toInt() ?? 0,
      pm25: (current['pm2_5'] as num?)?.toDouble() ?? 0.0,
      pm10: (current['pm10'] as num?)?.toDouble() ?? 0.0,
      grassPollen: (current['grass_pollen'] as num?)?.toInt() ?? 0,
      birchPollen: (current['birch_pollen'] as num?)?.toInt() ?? 0,
      olivePollen: (current['olive_pollen'] as num?)?.toInt() ?? 0,
      ragweedPollen: (current['ragweed_pollen'] as num?)?.toInt() ?? 0,
      uvIndex: (current['uv_index'] as num?)?.toDouble() ?? 0.0,
      timestamp: DateTime.tryParse(current['time'] ?? '') ?? DateTime.now(),
    );
  }

  /// AQI to category description
  String get aqiCategory {
    if (aqi <= 50) return 'Good';
    if (aqi <= 100) return 'Moderate';
    if (aqi <= 150) return 'Unhealthy for Sensitive';
    if (aqi <= 200) return 'Unhealthy';
    if (aqi <= 300) return 'Very Unhealthy';
    return 'Hazardous';
  }

  /// Combined pollen level (max of all types)
  int get maxPollenLevel {
    return [grassPollen, birchPollen, olivePollen, ragweedPollen]
        .reduce((a, b) => a > b ? a : b);
  }

  /// Pollen category based on max level
  String get pollenCategory {
    final level = maxPollenLevel;
    if (level < 10) return 'Low';
    if (level < 50) return 'Moderate';
    if (level < 100) return 'High';
    return 'Very High';
  }
}

/// Sun and moon data for visibility planning
class VisibilityData {
  final DateTime sunrise;
  final DateTime sunset;
  final double daylightDuration; // in hours
  final int moonPhase; // 0-7 (new moon to waning crescent)
  final DateTime timestamp;

  VisibilityData({
    required this.sunrise,
    required this.sunset,
    required this.daylightDuration,
    required this.moonPhase,
    required this.timestamp,
  });

  factory VisibilityData.fromJson(Map<String, dynamic> json) {
    final daily = json['daily'] as Map<String, dynamic>? ?? {};
    final sunriseList = daily['sunrise'] as List<dynamic>? ?? [];
    final sunsetList = daily['sunset'] as List<dynamic>? ?? [];
    final daylightList = daily['daylight_duration'] as List<dynamic>? ?? [];

    return VisibilityData(
      sunrise: sunriseList.isNotEmpty
          ? DateTime.tryParse(sunriseList[0] ?? '') ?? DateTime.now()
          : DateTime.now(),
      sunset: sunsetList.isNotEmpty
          ? DateTime.tryParse(sunsetList[0] ?? '') ?? DateTime.now()
          : DateTime.now(),
      daylightDuration: daylightList.isNotEmpty
          ? ((daylightList[0] as num?)?.toDouble() ?? 0.0) / 3600
          : 12.0,
      moonPhase: _calculateMoonPhase(),
      timestamp: DateTime.now(),
    );
  }

  /// Calculate approximate moon phase (0-7)
  static int _calculateMoonPhase() {
    final now = DateTime.now();
    // Known new moon: January 6, 2000
    final knownNewMoon = DateTime(2000, 1, 6);
    final daysSinceNewMoon = now.difference(knownNewMoon).inDays;
    final lunarCycle = 29.53; // days
    final phase = (daysSinceNewMoon % lunarCycle) / lunarCycle;
    return (phase * 8).floor() % 8;
  }

  /// Moon phase name
  String get moonPhaseName {
    switch (moonPhase) {
      case 0:
        return 'New Moon';
      case 1:
        return 'Waxing Crescent';
      case 2:
        return 'First Quarter';
      case 3:
        return 'Waxing Gibbous';
      case 4:
        return 'Full Moon';
      case 5:
        return 'Waning Gibbous';
      case 6:
        return 'Last Quarter';
      case 7:
        return 'Waning Crescent';
      default:
        return 'Unknown';
    }
  }

  /// Moon phase icon (using unicode)
  String get moonPhaseEmoji {
    switch (moonPhase) {
      case 0:
        return '🌑';
      case 1:
        return '🌒';
      case 2:
        return '🌓';
      case 3:
        return '🌔';
      case 4:
        return '🌕';
      case 5:
        return '🌖';
      case 6:
        return '🌗';
      case 7:
        return '🌘';
      default:
        return '🌑';
    }
  }

  /// Whether it's currently daytime
  bool get isDaytime {
    final now = DateTime.now();
    return now.isAfter(sunrise) && now.isBefore(sunset);
  }

  /// Time until sunrise/sunset
  Duration get timeUntilTransition {
    final now = DateTime.now();
    if (isDaytime) {
      return sunset.difference(now);
    } else {
      if (now.isBefore(sunrise)) {
        return sunrise.difference(now);
      } else {
        // After sunset, calculate time to next sunrise (tomorrow)
        return sunrise.add(const Duration(days: 1)).difference(now);
      }
    }
  }
}

/// Public resource from OpenStreetMap Overpass API
class PublicResource {
  final String id;
  final String name;
  final ResourceType type;
  final double latitude;
  final double longitude;
  final double? distanceMeters;
  final String? address;
  final String? phone;
  final String? website;
  final String? openingHours;

  PublicResource({
    required this.id,
    required this.name,
    required this.type,
    required this.latitude,
    required this.longitude,
    this.distanceMeters,
    this.address,
    this.phone,
    this.website,
    this.openingHours,
  });

  factory PublicResource.fromOverpassElement(
    Map<String, dynamic> element,
    double userLat,
    double userLng,
  ) {
    final tags = element['tags'] as Map<String, dynamic>? ?? {};
    final lat = (element['lat'] as num?)?.toDouble() ??
        (element['center']?['lat'] as num?)?.toDouble() ??
        0.0;
    final lng = (element['lon'] as num?)?.toDouble() ??
        (element['center']?['lon'] as num?)?.toDouble() ??
        0.0;

    // Calculate distance
    final distance = _calculateDistance(userLat, userLng, lat, lng);

    // Determine type
    ResourceType type = ResourceType.other;
    final amenity = tags['amenity'] as String?;
    final leisure = tags['leisure'] as String?;

    if (amenity == 'library') {
      type = ResourceType.library;
    } else if (amenity == 'hospital') {
      type = ResourceType.hospital;
    } else if (amenity == 'police') {
      type = ResourceType.police;
    } else if (amenity == 'pharmacy') {
      type = ResourceType.pharmacy;
    } else if (amenity == 'fire_station') {
      type = ResourceType.fireStation;
    } else if (leisure == 'park') {
      type = ResourceType.park;
    }

    return PublicResource(
      id: element['id']?.toString() ?? '',
      name: tags['name'] as String? ?? type.displayName,
      type: type,
      latitude: lat,
      longitude: lng,
      distanceMeters: distance,
      address: _buildAddress(tags),
      phone: tags['phone'] as String?,
      website: tags['website'] as String?,
      openingHours: tags['opening_hours'] as String?,
    );
  }

  static String? _buildAddress(Map<String, dynamic> tags) {
    final parts = <String>[];
    if (tags['addr:housenumber'] != null) {
      parts.add(tags['addr:housenumber'].toString());
    }
    if (tags['addr:street'] != null) {
      parts.add(tags['addr:street'].toString());
    }
    if (tags['addr:city'] != null) {
      parts.add(tags['addr:city'].toString());
    }
    return parts.isNotEmpty ? parts.join(', ') : null;
  }

  /// Haversine formula for distance calculation
  static double _calculateDistance(
    double lat1,
    double lon1,
    double lat2,
    double lon2,
  ) {
    const R = 6371000.0; // Earth's radius in meters
    final dLat = _toRadians(lat2 - lat1);
    final dLon = _toRadians(lon2 - lon1);
    final a = _sin(dLat / 2) * _sin(dLat / 2) +
        _cos(_toRadians(lat1)) *
            _cos(_toRadians(lat2)) *
            _sin(dLon / 2) *
            _sin(dLon / 2);
    final c = 2 * _atan2(_sqrt(a), _sqrt(1 - a));
    return R * c;
  }

  static double _toRadians(double deg) => deg * 3.141592653589793 / 180;
  static double _sin(double x) => _taylorSin(x);
  static double _cos(double x) => _taylorSin(x + 1.5707963267948966);
  static double _sqrt(double x) {
    if (x <= 0) return 0;
    double guess = x / 2;
    for (int i = 0; i < 10; i++) {
      guess = (guess + x / guess) / 2;
    }
    return guess;
  }

  static double _atan2(double y, double x) {
    if (x > 0) return _atan(y / x);
    if (x < 0 && y >= 0) return _atan(y / x) + 3.141592653589793;
    if (x < 0 && y < 0) return _atan(y / x) - 3.141592653589793;
    if (x == 0 && y > 0) return 1.5707963267948966;
    if (x == 0 && y < 0) return -1.5707963267948966;
    return 0;
  }

  static double _atan(double x) {
    if (x.abs() > 1) {
      return x > 0
          ? 1.5707963267948966 - _atan(1 / x)
          : -1.5707963267948966 - _atan(1 / x);
    }
    double result = 0;
    double term = x;
    for (int i = 1; i <= 15; i += 2) {
      result += term / i;
      term *= -x * x;
    }
    return result;
  }

  static double _taylorSin(double x) {
    // Normalize to [-pi, pi]
    while (x > 3.141592653589793) {
      x -= 6.283185307179586;
    }
    while (x < -3.141592653589793) {
      x += 6.283185307179586;
    }
    double result = 0;
    double term = x;
    for (int i = 1; i <= 15; i += 2) {
      result += term;
      term *= -x * x / ((i + 1) * (i + 2));
    }
    return result;
  }

  /// Formatted distance string
  String get formattedDistance {
    if (distanceMeters == null) return '';
    if (distanceMeters! < 1000) {
      return '${distanceMeters!.round()}m';
    }
    return '${(distanceMeters! / 1000).toStringAsFixed(1)}km';
  }
}

enum ResourceType {
  library,
  park,
  hospital,
  police,
  pharmacy,
  fireStation,
  other,
}

extension ResourceTypeExtension on ResourceType {
  String get displayName {
    switch (this) {
      case ResourceType.library:
        return 'Library';
      case ResourceType.park:
        return 'Park';
      case ResourceType.hospital:
        return 'Hospital';
      case ResourceType.police:
        return 'Police Station';
      case ResourceType.pharmacy:
        return 'Pharmacy';
      case ResourceType.fireStation:
        return 'Fire Station';
      case ResourceType.other:
        return 'Public Resource';
    }
  }

  String get iconName {
    switch (this) {
      case ResourceType.library:
        return 'local_library';
      case ResourceType.park:
        return 'park';
      case ResourceType.hospital:
        return 'local_hospital';
      case ResourceType.police:
        return 'local_police';
      case ResourceType.pharmacy:
        return 'local_pharmacy';
      case ResourceType.fireStation:
        return 'local_fire_department';
      case ResourceType.other:
        return 'place';
    }
  }
}

/// Combined local intel data
class LocalIntelData {
  final WeatherConditions? weather;
  final EnvironmentalHazards? hazards;
  final VisibilityData? visibility;
  final List<PublicResource> resources;
  final DateTime fetchedAt;
  final bool isLoading;
  final String? error;

  LocalIntelData({
    this.weather,
    this.hazards,
    this.visibility,
    this.resources = const [],
    DateTime? fetchedAt,
    this.isLoading = false,
    this.error,
  }) : fetchedAt = fetchedAt ?? DateTime.now();

  LocalIntelData copyWith({
    WeatherConditions? weather,
    EnvironmentalHazards? hazards,
    VisibilityData? visibility,
    List<PublicResource>? resources,
    DateTime? fetchedAt,
    bool? isLoading,
    String? error,
  }) {
    return LocalIntelData(
      weather: weather ?? this.weather,
      hazards: hazards ?? this.hazards,
      visibility: visibility ?? this.visibility,
      resources: resources ?? this.resources,
      fetchedAt: fetchedAt ?? this.fetchedAt,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }

  static LocalIntelData loading() {
    return LocalIntelData(isLoading: true);
  }

  static LocalIntelData withError(String error) {
    return LocalIntelData(error: error);
  }
}
