import Charts
import CodexBarCore
import SwiftUI

@MainActor
struct UsageBreakdownChartMenuView: View {
    enum PresentationState: Equatable {
        case empty
        case totalsOnly
        case chart
    }

    private struct Point: Identifiable {
        let id: String
        let date: Date
        let service: String
        let creditsUsed: Double

        init(date: Date, service: String, creditsUsed: Double) {
            self.date = date
            self.service = service
            self.creditsUsed = creditsUsed
            self.id = "\(service)-\(Int(date.timeIntervalSince1970))- \(creditsUsed)"
        }
    }

    private let breakdown: [OpenAIDashboardDailyBreakdown]
    private let now: Date
    private let calendar: Calendar
    private let width: CGFloat
    @State private var selectedDayKey: String?

    init(
        breakdown: [OpenAIDashboardDailyBreakdown],
        now: Date = Date(),
        calendar: Calendar = .current,
        width: CGFloat)
    {
        self.breakdown = breakdown
        self.now = now
        self.calendar = calendar
        self.width = width
    }

    var body: some View {
        let summary = OpenAIDashboardDailyBreakdown.recentUsageSummary(
            from: self.breakdown,
            now: self.now,
            calendar: self.calendar)
        let model = Self.makeModel(from: summary.daily)
        let presentationState = Self.presentationState(
            hasSummary: !summary.daily.isEmpty,
            hasChartPoints: !model.points.isEmpty)
        VStack(alignment: .leading, spacing: 10) {
            if presentationState != .empty {
                HStack(alignment: .firstTextBaseline) {
                    self.summaryMetric(title: L("Today"), credits: summary.todayCredits)
                    Spacer(minLength: 12)
                    self.summaryMetric(
                        title: String(format: L("Last %d days"), summary.historyDays),
                        credits: summary.totalCredits)
                }
            }

            if presentationState == .empty {
                Text(L("No usage breakdown data."))
                    .font(.footnote)
                    .foregroundStyle(.secondary)
                    .accessibilityLabel(L("No usage breakdown data available."))
            } else if presentationState == .chart {
                Chart {
                    ForEach(model.points) { point in
                        BarMark(
                            x: .value(L("Day"), point.date, unit: .day),
                            y: .value(L("Credits used"), point.creditsUsed))
                            .foregroundStyle(by: .value(L("Service"), point.service))
                    }
                    if let peak = model.peakPoint {
                        let capStart = max(peak.creditsUsed - Self.capHeight(maxValue: model.maxCreditsUsed), 0)
                        BarMark(
                            x: .value(L("Day"), peak.date, unit: .day),
                            yStart: .value(L("Cap start"), capStart),
                            yEnd: .value(L("Cap end"), peak.creditsUsed))
                            .foregroundStyle(Color(nsColor: .systemYellow))
                    }
                }
                .chartForegroundStyleScale(domain: model.services, range: model.serviceColors)
                .chartYAxis(.hidden)
                .chartXAxis {
                    AxisMarks(values: model.axisDates) { value in
                        AxisGridLine().foregroundStyle(Color.clear)
                        AxisTick().foregroundStyle(Color.clear)
                        if let date = value.as(Date.self) {
                            AxisValueLabel(anchor: Self.xAxisLabelAnchor(for: date, axisDates: model.axisDates)) {
                                Text(date, format: .dateTime.month(.abbreviated).day())
                                    .font(.caption2)
                                    .foregroundStyle(Color(nsColor: .tertiaryLabelColor))
                            }
                        }
                    }
                }
                .chartLegend(.hidden)
                .frame(height: 130)
                .accessibilityLabel(L("Usage breakdown chart"))
                .accessibilityValue(
                    model.points.isEmpty
                        ? L("No data")
                        : String(
                            format: L("%d days of usage data across %d services"),
                            model.points.count,
                            model.services.count))
                .chartOverlay { proxy in
                    GeometryReader { geo in
                        ZStack(alignment: .topLeading) {
                            if let rect = self.selectionBandRect(model: model, proxy: proxy, geo: geo) {
                                Rectangle()
                                    .fill(Self.selectionBandColor)
                                    .frame(width: rect.width, height: rect.height)
                                    .position(x: rect.midX, y: rect.midY)
                                    .allowsHitTesting(false)
                            }
                            MouseLocationReader { location in
                                self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
                            }
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .contentShape(Rectangle())
                        }
                    }
                }

                let detail = self.detailLines(model: model)
                VStack(alignment: .leading, spacing: 0) {
                    Text(detail.primary)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                        .truncationMode(.tail)
                        .frame(height: 16, alignment: .leading)
                    Text(detail.secondary ?? " ")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                        .truncationMode(.tail)
                        .frame(height: 16, alignment: .leading)
                        .opacity(detail.secondary == nil ? 0 : 1)
                }

                LazyVGrid(
                    columns: [GridItem(.adaptive(minimum: 110), alignment: .leading)],
                    alignment: .leading,
                    spacing: 6)
                {
                    ForEach(model.services, id: \.self) { service in
                        HStack(spacing: 6) {
                            Circle()
                                .fill(model.color(for: service))
                                .frame(width: 7, height: 7)
                            Text(service)
                                .font(.caption2)
                                .foregroundStyle(.secondary)
                                .lineLimit(1)
                        }
                    }
                }
            }
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
        .frame(minWidth: self.width, maxWidth: .infinity, alignment: .leading)
    }

    static func presentationState(hasSummary: Bool, hasChartPoints: Bool) -> PresentationState {
        if hasChartPoints { return .chart }
        if hasSummary { return .totalsOnly }
        return .empty
    }

    private struct Model {
        let points: [Point]
        let breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown]
        let dayDates: [(dayKey: String, date: Date)]
        let selectableDayDates: [(dayKey: String, date: Date)]
        let peakPoint: (date: Date, creditsUsed: Double)?
        let services: [String]
        let serviceColors: [Color]
        let axisDates: [Date]
        let maxCreditsUsed: Double

        func color(for service: String) -> Color {
            guard let idx = self.services.firstIndex(of: service), idx < self.serviceColors.count else {
                return .secondary
            }
            return self.serviceColors[idx]
        }
    }

    private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)

    private func summaryMetric(title: String, credits: Double?) -> some View {
        VStack(alignment: .leading, spacing: 1) {
            Text(title)
                .font(.caption2)
                .foregroundStyle(.secondary)
            Text(Self.creditsString(credits))
                .font(.subheadline)
                .fontWeight(.semibold)
                .monospacedDigit()
        }
        .accessibilityElement(children: .combine)
    }

    private static func creditsString(_ credits: Double?) -> String {
        guard let credits, credits.isFinite else { return "—" }
        let value = credits.formatted(.number.precision(.fractionLength(0...2)))
        return "\(value) \(L("credits"))"
    }

    private static func makeModel(from breakdown: [OpenAIDashboardDailyBreakdown]) -> Model {
        let sorted = OpenAIDashboardDailyBreakdown.removingSkillUsageServices(from: breakdown)
            .sorted { lhs, rhs in lhs.day < rhs.day }

        var points: [Point] = []
        points.reserveCapacity(sorted.count * 2)

        var breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown] = [:]
        breakdownByDayKey.reserveCapacity(sorted.count)

        var dayDates: [(dayKey: String, date: Date)] = []
        dayDates.reserveCapacity(sorted.count)

        var selectableDayDates: [(dayKey: String, date: Date)] = []
        selectableDayDates.reserveCapacity(sorted.count)

        var peak: (date: Date, creditsUsed: Double)?
        var maxCreditsUsed: Double = 0

        for day in sorted {
            guard let date = self.dateFromDayKey(day.day) else { continue }
            breakdownByDayKey[day.day] = day
            dayDates.append((dayKey: day.day, date: date))
            if day.totalCreditsUsed > 0 {
                if let cur = peak {
                    if day.totalCreditsUsed > cur.creditsUsed { peak = (date, day.totalCreditsUsed) }
                } else {
                    peak = (date, day.totalCreditsUsed)
                }
                maxCreditsUsed = max(maxCreditsUsed, day.totalCreditsUsed)
            }
            var addedSelectable = false
            for service in day.services where service.creditsUsed > 0 {
                points.append(Point(date: date, service: service.service, creditsUsed: service.creditsUsed))
                if !addedSelectable {
                    selectableDayDates.append((dayKey: day.day, date: date))
                    addedSelectable = true
                }
            }
        }

        let services = Self.serviceOrder(from: sorted)
        let colors = services.map { Self.colorForService($0) }
        let axisDates = Self.axisDates(fromSortedDays: sorted)

        return Model(
            points: points,
            breakdownByDayKey: breakdownByDayKey,
            dayDates: dayDates,
            selectableDayDates: selectableDayDates,
            peakPoint: peak,
            services: services,
            serviceColors: colors,
            axisDates: axisDates,
            maxCreditsUsed: maxCreditsUsed)
    }

    private static func capHeight(maxValue: Double) -> Double {
        maxValue * 0.05
    }

    private static func serviceOrder(from breakdown: [OpenAIDashboardDailyBreakdown]) -> [String] {
        var totals: [String: Double] = [:]
        for day in breakdown {
            for service in day.services {
                totals[service.service, default: 0] += service.creditsUsed
            }
        }

        return totals
            .sorted { lhs, rhs in
                if lhs.value == rhs.value { return lhs.key < rhs.key }
                return lhs.value > rhs.value
            }
            .map(\.key)
    }

    private static func colorForService(_ service: String) -> Color {
        let lower = service.lowercased()
        if lower == "cli" {
            return Color(red: 0.26, green: 0.55, blue: 0.96)
        }
        if lower.contains("github"), lower.contains("review") {
            return Color(red: 0.94, green: 0.53, blue: 0.18)
        }
        let palette: [Color] = [
            Color(red: 0.46, green: 0.75, blue: 0.36),
            Color(red: 0.80, green: 0.45, blue: 0.92),
            Color(red: 0.26, green: 0.78, blue: 0.86),
            Color(red: 0.94, green: 0.74, blue: 0.26),
        ]
        let idx = abs(service.hashValue) % palette.count
        return palette[idx]
    }

    private static func axisDates(fromSortedDays sortedDays: [OpenAIDashboardDailyBreakdown]) -> [Date] {
        guard let first = sortedDays.first, let last = sortedDays.last else { return [] }
        guard let firstDate = self.dateFromDayKey(first.day),
              let lastDate = self.dateFromDayKey(last.day)
        else {
            return []
        }
        if Calendar.current.isDate(firstDate, inSameDayAs: lastDate) {
            return [firstDate]
        }
        return [firstDate, lastDate]
    }

    private static func xAxisLabelAnchor(for date: Date, axisDates: [Date]) -> UnitPoint {
        if let first = axisDates.first, Calendar.current.isDate(date, inSameDayAs: first) {
            return .topLeading
        }
        if let last = axisDates.last, Calendar.current.isDate(date, inSameDayAs: last) {
            return .topTrailing
        }
        return .top
    }

    private static func dateFromDayKey(_ key: String) -> Date? {
        let parts = key.split(separator: "-")
        guard parts.count == 3,
              let year = Int(parts[0]),
              let month = Int(parts[1]),
              let day = Int(parts[2])
        else {
            return nil
        }

        var comps = DateComponents()
        comps.calendar = Calendar.current
        comps.timeZone = TimeZone.current
        comps.year = year
        comps.month = month
        comps.day = day
        // Noon avoids off-by-one-day shifts if anything ends up interpreted in UTC.
        comps.hour = 12
        return comps.date
    }

    private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
        guard let key = self.selectedDayKey else { return nil }
        guard let plotAnchor = proxy.plotFrame else { return nil }
        let plotFrame = geo[plotAnchor]
        guard let index = model.dayDates.firstIndex(where: { $0.dayKey == key }) else { return nil }
        let date = model.dayDates[index].date
        guard let x = proxy.position(forX: date) else { return nil }

        if model.dayDates.count <= 1 {
            return CGRect(
                x: plotFrame.origin.x,
                y: plotFrame.origin.y,
                width: plotFrame.width,
                height: plotFrame.height)
        }

        // Use the calendar day slot width (always 1 day on the time axis) so the band is the
        // same size for every bar regardless of gaps in the data.
        let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: date)) ?? (x + 20)
        let slotWidth = abs(nextDayX - x)
        let barHalfWidth = slotWidth * 0.25 + 2

        let left = plotFrame.origin.x + x - barHalfWidth
        let right = plotFrame.origin.x + x + barHalfWidth
        return CGRect(x: left, y: plotFrame.origin.y, width: right - left, height: plotFrame.height)
    }

    private func updateSelection(
        location: CGPoint?,
        model: Model,
        proxy: ChartProxy,
        geo: GeometryProxy)
    {
        guard let location else {
            if self.selectedDayKey != nil { self.selectedDayKey = nil }
            return
        }

        guard let plotAnchor = proxy.plotFrame else { return }
        let plotFrame = geo[plotAnchor]
        guard plotFrame.contains(location) else { return }

        let xInPlot = location.x - plotFrame.origin.x
        guard let date: Date = proxy.value(atX: xInPlot) else { return }
        guard let nearest = self.nearestDayKey(to: date, model: model) else { return }

        // Stay on the last selected bar when cursor is in the gap between bars; only switch
        // selection when the cursor is over the bar's own visual body.
        // Skip this gate for single-day charts: no gap exists, and selectionBandRect
        // already covers the full plot width in that case.
        if model.selectableDayDates.count > 1,
           let nearestEntry = model.selectableDayDates.first(where: { $0.dayKey == nearest }),
           let barX = proxy.position(forX: nearestEntry.date)
        {
            let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: nearestEntry.date)) ??
                (barX + 20)
            let slotWidth = abs(nextDayX - barX)
            guard ChartBarHoverSelection.accepts(
                distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
                barHalfWidth: slotWidth * 0.25 + 2,
                selectableCount: model.selectableDayDates.count)
            else { return }
        }

        if self.selectedDayKey != nearest {
            self.selectedDayKey = nearest
        }
    }

    private func nearestDayKey(to date: Date, model: Model) -> String? {
        guard !model.selectableDayDates.isEmpty else { return nil }
        var best: (key: String, distance: TimeInterval)?
        for entry in model.selectableDayDates {
            let dist = abs(entry.date.timeIntervalSince(date))
            if let cur = best {
                if dist < cur.distance { best = (entry.dayKey, dist) }
            } else {
                best = (entry.dayKey, dist)
            }
        }
        return best?.key
    }

    private func detailLines(model: Model) -> (primary: String, secondary: String?) {
        guard let key = self.selectedDayKey,
              let day = model.breakdownByDayKey[key],
              let date = Self.dateFromDayKey(key)
        else {
            return (L("Hover a bar for details"), nil)
        }

        let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
        let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2)))
        if day.services.isEmpty {
            return ("\(dayLabel): \(total)", nil)
        }
        if day.services.count <= 1, let first = day.services.first {
            let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2)))
            return ("\(dayLabel): \(used)", first.service)
        }

        let services = day.services
            .sorted { lhs, rhs in
                if lhs.creditsUsed == rhs.creditsUsed { return lhs.service < rhs.service }
                return lhs.creditsUsed > rhs.creditsUsed
            }
            .prefix(3)
            .map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" }
            .joined(separator: " · ")

        return ("\(dayLabel): \(total)", services)
    }
}
