// ABOUTME: Chart data service for aggregating health data for visualization
// ABOUTME: Provides data aggregation, trend calculation, smoothing, and export functionality for charts

import 'dart:math';
import 'package:collection/collection.dart';
import 'package:albi/models/weight_entry.dart';
import 'package:albi/models/blood_pressure_entry.dart';
import 'package:albi/models/mood_entry.dart';
import 'package:albi/services/database_service.dart';
import 'package:albi/utils/constants.dart';
import 'package:intl/intl.dart';

/// Time period options for chart data aggregation
enum ChartTimePeriod {
  oneMonth(Duration(days: 30), 'Last Month'),
  threeMonths(Duration(days: 90), 'Last 3 Months'),
  allTime(Duration(days: 36500), 'All Time'); // ~100 years

  const ChartTimePeriod(this.duration, this.displayName);
  
  final Duration duration;
  final String displayName;
}

/// Data point for chart visualization
class ChartDataPoint {
  final DateTime timestamp;
  final double value;
  final String? notes;
  final int? entryId;

  const ChartDataPoint({
    required this.timestamp,
    required this.value,
    this.notes,
    this.entryId,
  });

  @override
  String toString() => 'ChartDataPoint(timestamp: $timestamp, value: $value)';
}

/// Aggregated chart data with statistics
class ChartData {
  final List<ChartDataPoint> points;
  final List<ChartDataPoint>? trendLine;
  final ChartStatistics statistics;
  final String unit;

  const ChartData({
    required this.points,
    this.trendLine,
    required this.statistics,
    required this.unit,
  });

  bool get isEmpty => points.isEmpty;
  bool get isNotEmpty => points.isNotEmpty;
}

/// Statistical information about chart data
class ChartStatistics {
  final double? average;
  final double? minimum;
  final double? maximum;
  final double? trend; // Overall trend (slope)
  final double? standardDeviation;
  final int totalEntries;
  final double? weightChange; // Change from first to last entry

  const ChartStatistics({
    this.average,
    this.minimum,
    this.maximum,
    this.trend,
    this.standardDeviation,
    this.totalEntries = 0,
    this.weightChange,
  });

  String formatAverage(String unit) {
    if (average == null) return 'N/A';
    final decimalPlaces = unit == UnitConstants.stonesUnit ? 2 : 1;
    return '${average!.toStringAsFixed(decimalPlaces)} $unit';
  }

  String formatRange(String unit) {
    if (minimum == null || maximum == null) return 'N/A';
    final decimalPlaces = unit == UnitConstants.stonesUnit ? 2 : 1;
    return '${minimum!.toStringAsFixed(decimalPlaces)} - ${maximum!.toStringAsFixed(decimalPlaces)} $unit';
  }

  String formatWeightChange(String unit) {
    if (weightChange == null) return 'N/A';
    final decimalPlaces = unit == UnitConstants.stonesUnit ? 2 : 1;
    final sign = weightChange! >= 0 ? '+' : '';
    return '$sign${weightChange!.toStringAsFixed(decimalPlaces)} $unit';
  }
}

/// Service for processing health data for chart visualization
class ChartDataService {
  final DatabaseService _databaseService = DatabaseService.instance;

  /// Get weight chart data for a specific time period
  Future<ChartData> getWeightChartData({
    required ChartTimePeriod timePeriod,
    required String displayUnit,
    bool smoothData = false,
    bool includeTrend = true,
  }) async {
    // Calculate date range
    final endDate = DateTime.now();
    final startDate = timePeriod == ChartTimePeriod.allTime
        ? DateTime(1970) // Effectively all time
        : endDate.subtract(timePeriod.duration);

    // Get weight entries from database
    final entries = await _databaseService.getWeightEntries(
      startDate: startDate,
      endDate: endDate,
    );

    if (entries.isEmpty) {
      return ChartData(
        points: [],
        statistics: const ChartStatistics(),
        unit: displayUnit,
      );
    }

    // Convert entries to chart points in the requested unit
    final points = entries.reversed.map((entry) => ChartDataPoint(
      timestamp: entry.timestamp,
      value: entry.getWeightInUnit(displayUnit),
      notes: entry.notes,
      entryId: entry.id,
    )).toList();

    // Apply data smoothing if requested
    final processedPoints = smoothData ? _smoothData(points) : points;

    // Calculate statistics
    final statistics = _calculateStatistics(processedPoints);

    // Calculate trend line if requested
    List<ChartDataPoint>? trendLine;
    if (includeTrend && processedPoints.length >= 2) {
      trendLine = _calculateTrendLine(processedPoints);
    }

    return ChartData(
      points: processedPoints,
      trendLine: trendLine,
      statistics: statistics,
      unit: displayUnit,
    );
  }

  /// Get weight data aggregated by time buckets (daily, weekly, monthly)
  Future<ChartData> getAggregatedWeightData({
    required ChartTimePeriod timePeriod,
    required String displayUnit,
    required AggregationType aggregationType,
  }) async {
    final rawData = await getWeightChartData(
      timePeriod: timePeriod,
      displayUnit: displayUnit,
      smoothData: false,
      includeTrend: false,
    );

    if (rawData.isEmpty) return rawData;

    final aggregatedPoints = _aggregateDataPoints(
      rawData.points,
      aggregationType,
    );

    final statistics = _calculateStatistics(aggregatedPoints);

    return ChartData(
      points: aggregatedPoints,
      statistics: statistics,
      unit: displayUnit,
    );
  }

  /// Apply moving average smoothing to data points
  List<ChartDataPoint> _smoothData(List<ChartDataPoint> points, {int windowSize = 3}) {
    if (points.length <= windowSize) return points;

    final smoothedPoints = <ChartDataPoint>[];
    
    for (int i = 0; i < points.length; i++) {
      final start = max(0, i - windowSize ~/ 2);
      final end = min(points.length, i + windowSize ~/ 2 + 1);
      
      final windowPoints = points.sublist(start, end);
      final averageValue = windowPoints.map((p) => p.value).average;
      
      smoothedPoints.add(ChartDataPoint(
        timestamp: points[i].timestamp,
        value: averageValue,
        notes: points[i].notes,
        entryId: points[i].entryId,
      ));
    }
    
    return smoothedPoints;
  }

  /// Calculate trend line using linear regression
  List<ChartDataPoint> _calculateTrendLine(List<ChartDataPoint> points) {
    if (points.length < 2) return [];

    // Convert timestamps to numeric values (days since first point)
    final firstTimestamp = points.first.timestamp;
    final xValues = points.map((p) => 
        p.timestamp.difference(firstTimestamp).inDays.toDouble()).toList();
    final yValues = points.map((p) => p.value).toList();

    // Calculate linear regression
    final n = points.length;
    final sumX = xValues.sum;
    final sumY = yValues.sum;
    final sumXY = List.generate(n, (i) => xValues[i] * yValues[i]).sum;
    final sumXX = xValues.map((x) => x * x).sum;

    final slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
    final intercept = (sumY - slope * sumX) / n;

    // Generate trend line points
    return [
      ChartDataPoint(
        timestamp: points.first.timestamp,
        value: intercept + slope * xValues.first,
      ),
      ChartDataPoint(
        timestamp: points.last.timestamp,
        value: intercept + slope * xValues.last,
      ),
    ];
  }

  /// Calculate comprehensive statistics for chart data
  ChartStatistics _calculateStatistics(List<ChartDataPoint> points) {
    if (points.isEmpty) {
      return const ChartStatistics();
    }

    final values = points.map((p) => p.value).toList();
    final average = values.average;
    final minimum = values.min;
    final maximum = values.max;
    
    // Calculate standard deviation
    final variance = values.map((v) => pow(v - average, 2)).average;
    final standardDeviation = sqrt(variance);

    // Calculate overall trend (simple slope)
    double? trend;
    if (points.length >= 2) {
      final firstValue = points.first.value;
      final lastValue = points.last.value;
      final timeSpan = points.last.timestamp.difference(points.first.timestamp).inDays;
      trend = timeSpan > 0 ? (lastValue - firstValue) / timeSpan : 0;
    }

    // Calculate weight change from first to last
    final weightChange = points.length >= 2 
        ? points.last.value - points.first.value 
        : null;

    return ChartStatistics(
      average: average,
      minimum: minimum,
      maximum: maximum,
      trend: trend,
      standardDeviation: standardDeviation,
      totalEntries: points.length,
      weightChange: weightChange,
    );
  }

  /// Aggregate data points by time buckets
  List<ChartDataPoint> _aggregateDataPoints(
    List<ChartDataPoint> points,
    AggregationType aggregationType,
  ) {
    if (points.isEmpty) return [];

    final grouped = <String, List<ChartDataPoint>>{};
    final dateFormat = _getDateFormatForAggregation(aggregationType);

    // Group points by time bucket
    for (final point in points) {
      final key = dateFormat.format(point.timestamp);
      grouped.putIfAbsent(key, () => []).add(point);
    }

    // Aggregate each group
    final aggregatedPoints = <ChartDataPoint>[];
    
    for (final entry in grouped.entries) {
      final groupPoints = entry.value;
      final aggregatedValue = _aggregateValues(
        groupPoints.map((p) => p.value).toList(),
        aggregationType,
      );
      
      // Use the latest timestamp in the group
      final latestTimestamp = groupPoints
          .map((p) => p.timestamp)
          .reduce((a, b) => a.isAfter(b) ? a : b);
      
      aggregatedPoints.add(ChartDataPoint(
        timestamp: latestTimestamp,
        value: aggregatedValue,
      ));
    }

    // Sort by timestamp
    aggregatedPoints.sort((a, b) => a.timestamp.compareTo(b.timestamp));
    
    return aggregatedPoints;
  }

  /// Get appropriate date format for aggregation type
  DateFormat _getDateFormatForAggregation(AggregationType type) {
    switch (type) {
      case AggregationType.daily:
        return DateFormat('yyyy-MM-dd');
      case AggregationType.weekly:
        // Group by year and week number
        return DateFormat("yyyy-'W'ww");
      case AggregationType.monthly:
        return DateFormat('yyyy-MM');
    }
  }

  /// Aggregate values based on aggregation type
  double _aggregateValues(List<double> values, AggregationType type) {
    switch (type) {
      case AggregationType.daily:
      case AggregationType.weekly:
      case AggregationType.monthly:
        // For weight data, use average for all aggregation types
        return values.average;
    }
  }

  /// Export chart data as CSV format
  String exportChartDataAsCSV(ChartData chartData) {
    final buffer = StringBuffer();
    buffer.writeln('Timestamp,Weight (${chartData.unit}),Notes');
    
    for (final point in chartData.points) {
      final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(point.timestamp);
      final notes = (point.notes?.replaceAll(',', ';') ?? '').replaceAll('\n', ' ');
      buffer.writeln('$timestamp,${point.value.toStringAsFixed(2)},"$notes"');
    }
    
    return buffer.toString();
  }

  /// Get chart data for a custom date range
  Future<ChartData> getCustomRangeWeightData({
    required DateTime startDate,
    required DateTime endDate,
    required String displayUnit,
    bool smoothData = false,
    bool includeTrend = true,
  }) async {
    final entries = await _databaseService.getWeightEntries(
      startDate: startDate,
      endDate: endDate,
    );

    if (entries.isEmpty) {
      return ChartData(
        points: [],
        statistics: const ChartStatistics(),
        unit: displayUnit,
      );
    }

    final points = entries.reversed.map((entry) => ChartDataPoint(
      timestamp: entry.timestamp,
      value: entry.getWeightInUnit(displayUnit),
      notes: entry.notes,
      entryId: entry.id,
    )).toList();

    final processedPoints = smoothData ? _smoothData(points) : points;
    final statistics = _calculateStatistics(processedPoints);

    List<ChartDataPoint>? trendLine;
    if (includeTrend && processedPoints.length >= 2) {
      trendLine = _calculateTrendLine(processedPoints);
    }

    return ChartData(
      points: processedPoints,
      trendLine: trendLine,
      statistics: statistics,
      unit: displayUnit,
    );
  }

  /// Get blood pressure chart data for a specific time period
  Future<List<BloodPressureEntry>> getBloodPressureChartData({
    required ChartTimePeriod timePeriod,
  }) async {
    final endDate = DateTime.now();
    final startDate = timePeriod == ChartTimePeriod.allTime
        ? DateTime(1970)
        : endDate.subtract(timePeriod.duration);

    return await _databaseService.getBloodPressureEntries(
      startDate: startDate,
      endDate: endDate,
    );
  }

  /// Get mood chart data for a specific time period
  Future<List<MoodEntry>> getMoodChartData({
    required ChartTimePeriod timePeriod,
  }) async {
    final endDate = DateTime.now();
    final startDate = timePeriod == ChartTimePeriod.allTime
        ? DateTime(1970)
        : endDate.subtract(timePeriod.duration);

    return await _databaseService.getMoodEntries(
      startDate: startDate,
      endDate: endDate,
    );
  }
}

/// Types of data aggregation available
enum AggregationType {
  daily('Daily Average'),
  weekly('Weekly Average'),
  monthly('Monthly Average');

  const AggregationType(this.displayName);
  final String displayName;
}
