using System.Collections.ObjectModel;
using Microsoft.VisualBasic;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;

namespace SmartAquaViewer.ViewModel
{
    public class GraphControlViewModel
    {
        public PlotModel Model { get; }

        public GraphControlViewModel()
        {
            Model = new PlotModel();
            InitializeGraph();
        }

        private void InitializeGraph()
        {
            Model.TextColor = OxyColors.LightGray;
        }

        public void SetTankLineGraph(
            ReadOnlyObservableCollection<WaterQualityVO> collection,
            List<int>? selectedTankNums,
            FieldItem? xField, FieldItem? yField,
            bool isMarker, bool showLegends)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            bool xIsTime = string.Equals(xField!.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);

            Axis xAxis = xIsTime
                ? new DateTimeAxis
                {
                    Position = AxisPosition.Bottom,
                    Title = xField.Display,
                    StringFormat = "HH:mm:ss",
                    IntervalType = DateTimeIntervalType.Minutes,
                    MajorGridlineStyle = LineStyle.Solid,
                    MinorGridlineStyle = LineStyle.Dot,
                }
                : new LinearAxis
                {
                    Position = AxisPosition.Bottom,
                    Title = xField.Display,
                    MajorGridlineStyle = LineStyle.Solid,
                    MinorGridlineStyle = LineStyle.Dot
                };
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = yField!.Display,
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };

            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            var allTankIds = collection.SelectMany(w => w.Tanks)
                               .Select(t => t.Number)
                               .Distinct()
                               .OrderBy(id => id)
                               .ToList();
            var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
                ? allTankIds
                : allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();

            if (tankIds.Count == 0) { Model.InvalidatePlot(true); return; }

            var orderedCollection = collection.OrderBy(r => r.RecordedTime);

            foreach (var tankId in tankIds)
            {
                var series = new LineSeries()
                {
                    Title = tankId.ToString(),
                    MarkerType = isMarker ? MarkerType.Circle : MarkerType.None,
                    MarkerSize = isMarker ? 3 : 0
                };

                var points = new List<DataPoint>();
                foreach (var w in orderedCollection)
                {
                    var tank = w.Tanks.FirstOrDefault(t => t.Number == tankId);
                    if (tank is null) continue;

                    double? xVal = xIsTime
                        ? DateTimeAxis.ToDouble(w.RecordedTime)
                        : ResolveTank(tank, xField.Name!);       // 리플렉션: double? 반환

                    double? yVal = ResolveTank(tank, yField.Name!);

                    // ⚠️ 둘 중 하나라도 없으면 스킵
                    if (!xVal.HasValue || !yVal.HasValue) continue;

                    points.Add(new DataPoint(xVal.Value, yVal.Value));    
                }

                foreach (var p in points.OrderBy(p => p.X))
                    series.Points.Add(p);

                if (series.Points.Count > 0)
                {
                    var xTracker = xIsTime ? $"시간: {{2:HH:mm}}" : $"{xField.Display}: {{2:0.###}}";

                    // 트래커 포맷: 시간, 수조, 지표, 값
                    series.TrackerFormatString =
                        $"수조 {tankId}\n{xTracker}\n{yField.Display}: {{4:0.###}}";

                    Model.Series.Add(series);
                }
            }

            Model.Legends.Clear();
            Model.IsLegendVisible = showLegends;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = "수조",
            });
            Model.InvalidatePlot(true);
        }

        public void SetMultiLineGraph(
            List<WaterQualityVO> collection,
            ObservableCollection<FieldItem> yFields,
            DataType dataType,
            bool showMarker, bool showLegend)
        {
            Model.Series.Clear();
            Model.Axes.Clear();
            var xAxis = new DateTimeAxis
            {
                Position = AxisPosition.Bottom,
                Title = "시간",
                StringFormat = "HH:mm:ss",
                IntervalType = DateTimeIntervalType.Minutes,
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot,
            };
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = dataType == DataType.Energy ? "전력 (kW)" : "온실가스 (tCO₂)",
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };
            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            foreach (var field in yFields)
            {
                var series = new LineSeries()
                {
                    Title = field.Display,
                    MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
                    MarkerSize = showMarker ? 3 : 0
                };
                foreach (var r in collection.OrderBy(r => r.RecordedTime))
                {
                    double? y = null;
                    double? v = ResolveGreenhouseGas(r, field.Name!);
                    y = dataType == DataType.Energy ? ResolveEnergyField(r, field.Name!) : v;
                    if (!y.HasValue) continue;
                    series.Points.Add(new DataPoint(
                        DateTimeAxis.ToDouble(r.RecordedTime),
                        y.Value));
                }
                if (series.Points.Count > 0)
                {
                    // 트래커 포맷: 시간, 수조, 지표, 값
                    series.TrackerFormatString =
                        $"시간: {{2:HH:mm}}\n{field.Display}: {{4:0.###}}";
                    Model.Series.Add(series);
                }
            }

            Model.Legends.Clear();
            Model.IsLegendVisible = showLegend;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = dataType == DataType.Energy ? "전력 소비량" : "온실가스 배출량",
            });

            Model.InvalidatePlot(true);
        }

        public void SetDefaultLineGraph(
            List<WaterQualityVO> collection,
            MonitorTab selectedTab,
            FieldItem? xField, FieldItem? yField,
            bool isMarker)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            bool xIsTime = string.Equals(xField!.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);

            Axis xAxis = xIsTime
                ? new DateTimeAxis
                {
                    Position = AxisPosition.Bottom,
                    Title = xField.Display,
                    StringFormat = "HH:mm:ss",
                    IntervalType = DateTimeIntervalType.Minutes,
                    MajorGridlineStyle = LineStyle.Solid,
                    MinorGridlineStyle = LineStyle.Dot
                }
                : new LinearAxis
                {
                    Position = AxisPosition.Bottom,
                    Title = xField.Display,
                    MajorGridlineStyle = LineStyle.Solid,
                    MinorGridlineStyle = LineStyle.Dot
                };
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = yField!.Display,
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };

            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            var series = new LineSeries()
            {
                MarkerType = isMarker ? MarkerType.Circle : MarkerType.None,
                MarkerSize = isMarker ? 3 : 0
            };

            var points = new List<DataPoint>();

            foreach (var r in collection.OrderBy(r => r.RecordedTime))
            {
                double? xVal = xIsTime
                        ? DateTimeAxis.ToDouble(r.RecordedTime)
                        : double.NaN;
                if (xVal.HasValue && double.IsNaN(xVal.Value))
                {
                    xVal = selectedTab switch
                    {
                        MonitorTab.Filter => ResolveFilter(r, xField.Name!),
                        MonitorTab.Sterilizer => ResolveSterilizer(r, xField.Name!),
                        _ => xVal
                    };
                }

                double? y = null;
                if (selectedTab.Equals(MonitorTab.Filter))
                    y = ResolveFilter(r, yField.Name!);
                else if (selectedTab.Equals(MonitorTab.Sterilizer))
                    y = ResolveSterilizer(r, yField.Name!);                    

                if (!y.HasValue) continue;

                points.Add(new DataPoint(
                    xVal!.Value,
                    y.Value));
            }

            foreach (var p in points.OrderBy(p => p.X))
                series.Points.Add(p);

            if (series.Points.Count > 0)
            {
                var xTracker = xIsTime ? $"시간: {{2:HH:mm}}" : $"{xField.Display}: {{2:0.###}}";

                // 트래커 포맷: 시간, 수조, 지표, 값
                series.TrackerFormatString =
                    $"{xTracker}\n{yField.Display}: {{4:0.###}}";
            }

            Model.Series.Add(series);
            Model.InvalidatePlot(true);
        }

        public void SetBoxPlot(
            ReadOnlyObservableCollection<WaterQualityVO> collection,
            List<int>? selectedTankNums,
            FieldItem xAxisKind,                 // 시간 or 수조
            FieldItem valueField,                // 값 필드
            double boxWidth,                     // 박스 너비
            TimeSpan? timeBucket = null,         // 시간 사용할 때 버킷(기본 1시간)
            int maxCategories = 24               // X 카테고리 최대개수(너무 많을 때 최근 N개만)
        )
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            // 축
            var xAxis = new CategoryAxis
            {
                Position = AxisPosition.Bottom,
                GapWidth = 0.3,
                IsPanEnabled = false,
                IsZoomEnabled = false
            };
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = valueField.Name,
                MinorGridlineStyle = LineStyle.Dot,
                MajorGridlineStyle = LineStyle.Solid
            };
            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            var timeBuckets = collection
                .Select(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket))
                .Distinct()
                .OrderBy(t => t)
                .ToList();

            // X축 라벨
            xAxis.Labels.AddRange(timeBuckets.Select(t => t.ToString("HH:mm:ss")));

            var allTankIds = collection.SelectMany(w => w.Tanks).Select(t => t.Number).Distinct().OrderBy(id => id).ToList();
            var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
                ? allTankIds
                : allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();

            if (tankIds.Count == 0 || timeBuckets.Count == 0)
            {
                Model.InvalidatePlot(true);
                return;
            }

            // 3) 팔레트
            var colors = OxyPalettes.Hot(tankIds.Count).Colors;

            for (int k = 0; k < tankIds.Count; k++)
            {
                int tankId = tankIds[k];

                var series = new BoxPlotSeries
                {
                    Title = $"Tank {tankId}",
                    BoxWidth = boxWidth,
                    Fill = OxyColor.FromAColor(160, colors[k]),
                    Stroke = colors[k],
                    StrokeThickness = 1
                };

                for (int ti = 0; ti < timeBuckets.Count; ti++)
                {
                    var bucketTime = timeBuckets[ti];

                    // 해당 시간 버킷 + 해당 수조 + 선택 필드 값 모으기
                    var values = collection
                        .Where(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket!) == bucketTime)
                        .SelectMany(w => w.Tanks)
                        .Where(t => t.Number == tankId)
                        .Select(t => ResolveTank(t, valueField.Name!))  // 선택 필드 동적 접근
                        .Where(v => v.HasValue)
                        .Select(v => v.Value)
                        .ToList();

                    if (values.Count < 1) continue;

                    var item = CreateBoxPlotItem(ti, values);
                    if (item != null) series.Items.Add(item);
                }

                if (series.Items.Count > 0)
                    Model.Series.Add(series);
            }

            Model.Legends.Clear();
            Model.IsLegendVisible = true;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = "수조",
            });

            Model.InvalidatePlot(true);
        }

        public void SetScatterPlot(
            ReadOnlyObservableCollection<WaterQualityVO> collection,
            FieldItem xAxisField,
            FieldItem yAxisField,
            List<int> selectedTankNums,
            double markerSize = 3,
            bool showRegression = false,
            bool showLegends = true)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            bool xIsTime = string.Equals(xAxisField.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);

            Axis xAxis = xIsTime
                ? new DateTimeAxis 
                { 
                    Position = AxisPosition.Bottom, 
                    Title = xAxisField.Display,
                    StringFormat = "MM-dd\nHH:mm" 
                }
                : new LinearAxis 
                { 
                    Position = AxisPosition.Bottom,
                    Title = xAxisField.Display,
                };
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = yAxisField.Display,
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };

            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            var allTankIds = collection.SelectMany(w => w.Tanks)
                               .Select(t => t.Number)
                               .Distinct()
                               .OrderBy(id => id)
                               .ToList();
            var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
                ? allTankIds
                : allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();

            if (tankIds.Count == 0) { Model.InvalidatePlot(true); return; }

            // 색상/마커
            var colors = OxyPalettes.Hot(tankIds.Count).Colors;
            var markerCycle = new[]
            {
                MarkerType.Circle, MarkerType.Square, MarkerType.Triangle, MarkerType.Diamond,
                MarkerType.Plus, MarkerType.Star, MarkerType.Cross
            };

            int k = 0;
            foreach (var tankId in tankIds)
            {
                var series = new ScatterSeries
                {
                    Title = $"Tank {tankId}",
                    MarkerType = markerCycle[k % markerCycle.Length],
                    MarkerSize = markerSize,
                    // 색상 자동 배정에 맡기려면 MarkerFill 설정 생략해도 OK
                    MarkerFill = OxyColor.FromAColor(160, colors[k]), // 약간 투명
                };

                // 포인트 수집 (시간순 정렬 권장)
                var points = new List<ScatterPoint>();
                foreach (var w in collection.OrderBy(r => r.RecordedTime))
                {
                    foreach (var t in w.Tanks)
                    {
                        if (t.Number != tankId) continue;

                        // X
                        double? xVal = xIsTime
                            ? DateTimeAxis.ToDouble(w.RecordedTime)
                            : ResolveTank(t, xAxisField.Name!);   // 수조 객체의 선택필드(리플렉션)

                        // Y
                        double? yVal = ResolveTank(t, yAxisField.Name!);

                        if (xVal.HasValue && yVal.HasValue)
                            points.Add(new ScatterPoint(xVal.Value, yVal.Value));
                    }
                }

                // 포인트 반영
                if (points.Count > 0)
                {
                    series.Points.AddRange(points);
                    series.TrackerFormatString =
                        $"수조: {tankId}\nX: {{2:0.###}}\nY: {{4:0.###}}";

                    Model.Series.Add(series);

                    // 옵션: 수조별 선형회귀선
                    if (showRegression && !xIsTime && points.Count >= 2)
                    {
                        var (a, b) = FitLinear(points); // y = a*x + b
                        double minX = points.Min(p => p.X);
                        double maxX = points.Max(p => p.X);

                        var reg = new LineSeries
                        {
                            Title = $"Tank {tankId} 회귀",
                            StrokeThickness = 2,
                            Color = colors[k]
                        };
                        reg.Points.Add(new DataPoint(minX, a * minX + b));
                        reg.Points.Add(new DataPoint(maxX, a * maxX + b));
                        Model.Series.Add(reg);
                    }
                }

                k++;
            }

            Model.Legends.Clear();
            Model.IsLegendVisible = showLegends;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = "수조",
            });

            Model.InvalidatePlot(true);
        }

        public void SetStepPlot(
            List<WaterQualityVO> rows,
            MonitorTab selectedTab,
            FieldItem xAxisField,
            ObservableCollection<FieldItem> yAxisFields,
            bool showMarker = false, bool showLegends = true
            )
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            var xAxis = new CategoryAxis
            {
                Position = AxisPosition.Bottom,
                Title = "시간",
                GapWidth = 0.2,
                Angle = 45
            };

            foreach (var r in rows)
            {
                xAxis.Labels.Add(r.RecordedTime.ToString("HH:mm:ss"));
            }
            Model.Axes.Add(xAxis);

            Model.Axes.Add(new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "값"
            });

            foreach (var field in yAxisFields)
            {
                var series = new StairStepSeries
                {
                    Title = field.Display,
                    MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
                    MarkerSize = 3
                };

                int i = 0;
                foreach (var r in rows.OrderBy(r => r.RecordedTime))
                {
                    double? y = selectedTab.Equals(MonitorTab.Filter)
                        ? ResolveFilter(r, field.Name!)
                        : ResolveSterilizer(r, field.Name!);
                    if (y.HasValue)
                        series.Points.Add(new DataPoint(i, y.Value));

                    i++;
                }

                Model.Series.Add(series);
            }

            Model.InvalidatePlot(true);
        }

        public void SetStatusSeriesStopPlot(
            List<WaterQualityVO> collection, 
            FieldItem yAxisField,
            ObservableCollection<FieldItem> yAxisFields,
            bool showMarker = false, bool showLegends = true)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            var xAxis = new CategoryAxis
            {
                Position = AxisPosition.Bottom,
                Title = "시간",
                GapWidth = 0.2
            };

            foreach (var r in collection)
            {
                xAxis.Labels.Add(r.RecordedTime.ToString("HH:mm:ss"));
            }
            Model.Axes.Add(xAxis);

            Model.Axes.Add(new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "값",
            });

            foreach (var field in yAxisFields)
            {
                var series = new StairStepSeries
                {
                    Title = field.Display,
                    MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
                    MarkerSize = 3
                };

                int i = 0;
                foreach (var r in collection.OrderBy(r => r.RecordedTime))
                {
                    string? rawValue = ResolveStatus(r, field.Name!);
                    if (rawValue != null)
                    {
                        double y = MapDeviceStatus(rawValue);
                        series.Points.Add(new DataPoint(i, y));
                    }
                    else
                    {
                        double? uvPower = ResolveUvPowerPerId(r, field.Name!);
                        if (uvPower.HasValue)
                        {
                            series.Points.Add(new DataPoint(i, uvPower.Value));
                        }
                    }
                    i++;
                }

                Model.Series.Add(series);
            }

            Model.Legends.Clear();
            Model.IsLegendVisible = showLegends;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = "수조",
            });

            Model.InvalidatePlot(true);
        }

        public void SetStackAreaPlot(List<WaterQualityVO> collection, ObservableCollection<FieldItem> yFields,
            DataType dataType,
            bool showMarker, bool showLegends)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            // X축: 시간
            var xAxis = new DateTimeAxis
            {
                Position = AxisPosition.Bottom,
                Title = "시간",
                StringFormat = "HH:mm:ss",
                IntervalType = DateTimeIntervalType.Minutes,
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };
            // Y축: 전력
            var yAxis = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = dataType == DataType.Energy ? "전력 (kW)" : "온실가스 (tCO₂)",
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            };

            Model.Axes.Add(xAxis);
            Model.Axes.Add(yAxis);

            // 시간순 정렬
            var records = collection.OrderBy(r => r.RecordedTime).ToList();
            int n = records.Count;

            // 누적 버퍼
            double[] prevStack = new double[n];

            foreach (var field in yFields)
            {
                var area = new AreaSeries
                {
                    Title = field.Display,
                    StrokeThickness = 1,
                    MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
                };

                var upper = new List<DataPoint>();
                var lower = new List<DataPoint>();

                for (int i = 0; i < n; i++)
                {
                    double? y = dataType == DataType.Energy ? 
                        ResolveEnergyField(records[i], field.Name!) : 
                        ResolveGreenhouseGas(records[i], field.Name!);
                    double value = y ?? 0;

                    double x = DateTimeAxis.ToDouble(records[i].RecordedTime);

                    // 현재 값 누적
                    double bottom = prevStack[i];
                    double top = bottom + value;

                    lower.Add(new DataPoint(x, bottom));
                    upper.Add(new DataPoint(x, top));

                    prevStack[i] = top;
                }

                area.Points.AddRange(upper);   // 위쪽 경계
                area.Points2.AddRange(lower);  // 아래쪽 경계
                Model.Series.Add(area);
            }

            // 레전드
            Model.Legends.Clear();
            Model.IsLegendVisible = showLegends;
            Model.Legends.Add(new Legend
            {
                LegendPlacement = LegendPlacement.Outside,
                LegendPosition = LegendPosition.RightTop,
                LegendOrientation = LegendOrientation.Vertical,
                LegendTitle = dataType == DataType.Energy ? "전력 소비량" : "온실가스 배출량",
            });

            Model.InvalidatePlot(true);
        }

        public void SetPieChart(List<WaterQualityVO> collection, ObservableCollection<FieldItem> fields,
            DataType dataType,
            bool useAverage = false, bool donut = true,
            double minLabelPercent = 0.03)
        {
            Model.Series.Clear();
            Model.Axes.Clear();

            if (collection == null || collection.Count == 0)
            {
                Model.InvalidatePlot(true);
                return;
            }

            if (fields.Count == 0)
            {
                Model.InvalidatePlot(true);
                return;
            }

            var agg = new List<(string name, double value)>();
            foreach (var f in fields)
            {
                var values = collection.Select(r => (dataType == DataType.Energy ? ResolveEnergyField(r, f.Name!) : ResolveGreenhouseGas(r, f.Name!)) ?? 0.0);
                double v = (useAverage ? (values.Any() ? values.Average() : 0.0) : values.Sum());
                agg.Add((f.Display ?? f.Name!, v));
            }

            List<(string name, double value)> finalList;
            finalList = agg;

            double total = Math.Max(1e-9, finalList.Sum(x => x.value));

            var ps = new PieSeries
            {
                AngleSpan = 360,
                StartAngle = 0,
                StrokeThickness = 0.5,
                InsideLabelFormat = "{1}\n {0:F2}",
                InsideLabelPosition = 0.8,
                OutsideLabelFormat = null // 라벨은 내부만
            };
            if (donut) ps.InnerDiameter = 0.6; // 도넛 모드

            foreach (var (name, value) in finalList)
            {
                var pct = value / total;
                var label = (pct >= minLabelPercent) ? $"{name} ({pct:P0})" : ""; // 작은 조각 라벨 숨김
                ps.Slices.Add(new PieSlice(name, value) { });
            }

            Model.Title = $"설비별 {(useAverage ? "평균" : "합계")} 소비 비중";
            Model.Series.Add(ps);

            Model.InvalidatePlot(true);
        }

        private BoxPlotItem CreateBoxPlotItem(int xIndex, List<double> values)
        {
            values.Sort();
            int n = values.Count;
            if (n == 0) return null;

            double q1 = values[(int)(0.25 * (n - 1))];
            double q3 = values[(int)(0.75 * (n - 1))];
            double median = values[(int)(0.5 * (n - 1))];
            double iqr = q3 - q1;
            double lower = values.Where(v => v >= q1 - 1.5 * iqr).DefaultIfEmpty(q1).Min();
            double upper = values.Where(v => v <= q3 + 1.5 * iqr).DefaultIfEmpty(q3).Max();

            return new BoxPlotItem(xIndex, lower, q1, median, q3, upper)
            {
                Outliers = values.Where(v => v < lower || v > upper).ToList()
            };
        }

        private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
        {
            long ticks = bucket.Ticks;
            long floored = dt.Ticks - (dt.Ticks % ticks);
            return new DateTime(floored, dt.Kind);
        }

        private double? ResolveTank(WaterTank tank, string fieldName)
        {
            return fieldName switch
            {
                "DOValue" => tank.DOValue,
                "PH" => tank.PH,
                "ORP" => tank.ORP,
                "Temperature" => tank.Temperature,
                "FlowRate" => tank.FlowRate,
                _ => null
            };
        }

        private double? ResolveFilter(WaterQualityVO vo, string fieldName)
        {
            return fieldName switch
            {
                "Filtering.SumpPH" => vo.Filtering.SumpPH,
                "Filtering.SumpORP" => vo.Filtering.SumpORP,
                "Filtering.SumpWaterLevel" => vo.Filtering.SumpWaterLevel,
                "Filtering.SumpFlowRate" => vo.Filtering.SumpFlowRate,
                "Filtering.SumpTemperature" => vo.Filtering.SumpTemperature,
                "Filtering.FlowRate" => vo.Filtering.FlowRate,
                "Filtering.HeatPumpTemperature" => vo.Filtering.HeatPumpTemperature,
                _ => null
            };
        }

        private double? ResolveSterilizer(WaterQualityVO vo, string fieldName)
        {
            return fieldName switch
            {
                "Sterilizing.OzoneDissolverPressure" => vo.Sterilizing.OzoneDissolverPressure,
                _ => null
            };
        }

        private string? ResolveStatus(WaterQualityVO vo, string field)
        {
            return field switch
            {
                "Filtering.SandFilterPower" => vo.Filtering.SandFilterPower.ToString(),
                "Filtering.CirculationPumpPower" => vo.Filtering.CirculationPumpPower.ToString(),
                "Filtering.InverterControllerStatus" => vo.Filtering.InverterControllerStatus,
                "Filtering.HeatPumpPower" => vo.Filtering.HeatPumpPower.ToString(),
                "Filtering.AirBlowerPower" => vo.Filtering.AirBlowerPower.ToString(),
                "Sterilizing.OzoneGeneratorPower" => vo.Sterilizing.OzoneGeneratorPower.ToString(),
                "Sterilizing.OzoneDissolverPower" => vo.Sterilizing.OzoneDissolverPower.ToString(),
                "Sterilizing.ExcessOzoneDestroyerPower" => vo.Sterilizing.ExcessOzoneDestroyerPower.ToString(),
                _ => null
            };
        }

        private string ResolveTankOrTime(WaterQualityVO data, string xAxisKind, TimeSpan bucket)
        {
            int? tankId = data.Tanks?.FirstOrDefault().Number; // 분리된 구조이므로 항상 하나만 존재해야 함

            return xAxisKind switch
            {
                "수조" or "Tank" => tankId?.ToString() ?? "Unknown",

                "시간" or "RecordedTime" =>
                    $"{FloorToBucket(data.RecordedTime, bucket):yyyy-MM-dd HH:mm} | Tank {tankId}",

                _ => "Unknown"
            };
        }

        private double? ResolveEnergyField(WaterQualityVO vo, string fieldName)
        {
            return fieldName switch
            {
                "Filtering.SandFilterEnergy" => vo.Filtering.SandFilterEnergy,
                "Filtering.CirculationPumpEnergy" => vo.Filtering.CirculationPumpEnergy,
                "Filtering.HeatPumpEnergy" => vo.Filtering.HeatPumpEnergy,
                "Filtering.AirBlowerEnergy" => vo.Filtering.AirBlowerEnergy,
                "Sterilizing.OzoneGeneratorEnergy" => vo.Sterilizing.OzoneGeneratorEnergy,
                "Sterilizing.UVSterilizerEnergy" => vo.Sterilizing.UVSterilizerEnergy,
                "Sterilizing.OzoneDissolverEnergy" => vo.Sterilizing.OzoneDissolverEnergy,
                "Sterilizing.ExcessOzoneDestroyerEnergy" => vo.Sterilizing.ExcessOzoneDestroyerEnergy,
                "TotalEnergy" => vo.TotalEnergy,
                _ => null
            };
        }

        private double? ResolveGreenhouseGas(WaterQualityVO vo, string fieldName)
        {
            return fieldName switch
            {
                "Filtering.SandFilterGreenhouseGas" => vo.Filtering.SandFilterGreenhouseGas,
                "Filtering.CirculationPumpGreenhouseGas" => vo.Filtering.CirculationPumpGreenhouseGas,
                "Filtering.HeatPumpGreenhouseGas" => vo.Filtering.HeatPumpGreenhouseGas,
                "Filtering.AirBlowerGreenhouseGas" => vo.Filtering.AirBlowerGreenhouseGas,
                "Sterilizing.OzoneGeneratorGreenhouseGas" => vo.Sterilizing.OzoneGeneratorGreenhouseGas,
                "Sterilizing.UVSterilizerGreenhouseGas" => vo.Sterilizing.UVSterilizerGreenhouseGas,
                "Sterilizing.OzoneDissolverGreenhouseGas" => vo.Sterilizing.OzoneDissolverGreenhouseGas,
                "Sterilizing.ExcessOzoneDestroyerGreenhouseGas" => vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas,
                "TotalGreenhouseGas" => vo.TotalGreenhouseGas,
                _ => null
            };
        }

        private double? ResolveUvPowerPerId(WaterQualityVO r, string fieldName)
        {
            // 케이스 A
            if (fieldName.StartsWith("Sterilizing.UVSterilizerPower[id="))
            {
                var id = fieldName
                    .Replace("Sterilizing.UVSterilizerPower[id=", "")
                    .TrimEnd(']');

                if (r?.Sterilizing?.UVSterilizerId == id)
                    return MapStatus(r.Sterilizing.UVSterilizerPower);

                // 해당 시점에 이 ID의 레코드가 없으면 null (직전값 유지 로직에서 커버)
                return null;
            }

            return null;
        }

        private static double MapStatus(object? v)
        {
            return v switch
            {
                bool b => b ? 1 : 0,
                int i => i,                // 이미 0/1/2로 들어온 경우
                string s => s.Equals("True", StringComparison.OrdinalIgnoreCase) ? 1 :
                            s.Equals("False", StringComparison.OrdinalIgnoreCase) ? 0 :
                            s.Equals("Normal", StringComparison.OrdinalIgnoreCase) ? 1 :
                            s.Equals("Error", StringComparison.OrdinalIgnoreCase) ? 2 : -1,
                _ => -1
            };
        }


        private static string FormatBucket(DateTime t, TimeSpan bucket)
        {
            if (bucket >= TimeSpan.FromDays(7)) return t.ToString("yyyy-MM-dd");
            if (bucket >= TimeSpan.FromDays(1)) return t.ToString("MM-dd");
            return t.ToString("HH:00");
        }

        private double Percentile(IList<double> values, double p)
        {
            if (values == null || values.Count == 0) return double.NaN;
            if (values.Count == 1) return (double)values[0];

            double rank = p * (values.Count - 1);
            int i = (int)Math.Floor(rank);
            double frac = rank - i;

            if (i >= values.Count - 1) return (double)values.Last();
            return (double)(values[i] + (values[i + 1] - values[i]) * frac);
        }

        // 단순 선형회귀 계산
        private (double Slope, double Intercept)? LinearRegression(List<DataPoint> points)
        {
            int n = points.Count;
            if (n < 2) return null;

            double avgX = points.Average(p => p.X);
            double avgY = points.Average(p => p.Y);

            double numerator = points.Sum(p => (p.X - avgX) * (p.Y - avgY));
            double denominator = points.Sum(p => Math.Pow(p.X - avgX, 2));

            if (denominator == 0) return null;

            double slope = numerator / denominator;
            double intercept = avgY - slope * avgX;

            return (slope, intercept);
        }

        private (double a, double b) FitLinear(IEnumerable<ScatterPoint> pts)
        {
            int n = 0;
            double sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;
            foreach (var p in pts) { n++; sumX += p.X; sumY += p.Y; sumXX += p.X * p.X; sumXY += p.X * p.Y; }
            double denom = n * sumXX - sumX * sumX;
            if (n < 2 || Math.Abs(denom) < 1e-12) return (0, pts.First().Y); // 수직/특이 케이스
            double a = (n * sumXY - sumX * sumY) / denom;
            double b = (sumY - a * sumX) / n;
            return (a, b);
        }

        private double MapDeviceStatus(string status)
        {
            return status switch
            {
                "True" => 1,        // 전원 ON
                "False" => 0,       // 전원 OFF
                "Normal" => 1,      // 정상
                "Error" => 2,       // 오류
                _ => -1             // 알 수 없음
            };
        }

        private class KeyComparer : IComparer<string>
        {
            public int Compare(string? a, string? b)
            {
                if (a == null || b == null) return Comparer<string>.Default.Compare(a, b);
                if (a.StartsWith("수조 ") && b.StartsWith("수조 "))
                {
                    if (int.TryParse(a.AsSpan(3), out var ai) && int.TryParse(b.AsSpan(3), out var bi))
                        return ai.CompareTo(bi);
                }
                return string.Compare(a, b, StringComparison.Ordinal);
            }
        }
    }
}
