import SwiftUI

/// Static progress fill with no implicit animations, used inside the menu card.
struct UsageProgressBar: View {
    enum MarkerKind: Equatable {
        case quotaWarning
        case workdayBoundary
    }

    struct Marker: Equatable {
        let percent: Double
        let kind: MarkerKind
    }

    private static let paceStripeCount = 3
    private static let stripePunchOpacity = 0.9

    private nonisolated static var warningMarkerPunchWidth: CGFloat {
        5
    }

    private nonisolated static var warningMarkerStripeWidth: CGFloat {
        1
    }

    private static func paceStripeWidth(for scale: CGFloat) -> CGFloat {
        2
    }

    private static func paceStripeSpan(for scale: CGFloat) -> CGFloat {
        let stripeCount = max(1, Self.paceStripeCount)
        return Self.paceStripeWidth(for: scale) * CGFloat(stripeCount)
    }

    let percent: Double
    let tint: Color
    let accessibilityLabel: String
    let pacePercent: Double?
    let paceOnTop: Bool
    let warningMarkerPercents: [Double]
    let workdayMarkerPercents: [Double]
    let workdayTickAppearance: WorkdayTickAppearance
    @Environment(\.menuItemHighlighted) private var isHighlighted
    @Environment(\.displayScale) private var displayScale

    init(
        percent: Double,
        tint: Color,
        accessibilityLabel: String,
        pacePercent: Double? = nil,
        paceOnTop: Bool = true,
        warningMarkerPercents: [Double] = [],
        workdayMarkerPercents: [Double] = [],
        workdayTickAppearance: WorkdayTickAppearance = .subtle)
    {
        self.percent = percent
        self.tint = tint
        self.accessibilityLabel = accessibilityLabel
        self.pacePercent = pacePercent
        self.paceOnTop = paceOnTop
        self.warningMarkerPercents = warningMarkerPercents
        self.workdayMarkerPercents = workdayMarkerPercents
        self.workdayTickAppearance = workdayTickAppearance
    }

    private var clamped: Double {
        min(100, max(0, self.percent))
    }

    var body: some View {
        // Draw the entire progress bar — track, fill, and pace-tip punch-out — in a single Canvas.
        // A single Canvas uses Core Graphics internally and avoids the SwiftUI compositing modifiers
        // (.compositingGroup, .blendMode) that trigger Metal/RenderBox shader compilation on macOS 26.x,
        // which caused the status item icon to disappear (issue #805).
        Canvas { context, size in
            let scale = max(self.displayScale, 1)
            let fillPercent = Self.renderedFillPercent(self.clamped)
            let fillWidth = size.width * fillPercent / 100
            let paceWidth = size.width * Self.clampedPercent(self.pacePercent) / 100
            let tipWidth = max(25, size.height * 6.5)
            let stripeInset = 1 / scale
            let tipOffset = paceWidth - tipWidth + (Self.paceStripeSpan(for: scale) / 2) + stripeInset
            let showTip = self.pacePercent != nil && tipWidth > 0.5
            let markers = Self.resolvedMarkers(
                warningPercents: self.warningMarkerPercents,
                workdayPercents: self.workdayMarkerPercents,
                workdayAppearance: self.workdayTickAppearance)

            let cornerRadius = size.height / 2
            let cornerSize = CGSize(width: cornerRadius, height: cornerRadius)
            let rect = CGRect(origin: .zero, size: size)

            context.clip(to: Path(rect))

            // Track
            let trackPath = Path { p in p.addRoundedRect(in: rect, cornerSize: cornerSize) }
            context.fill(trackPath, with: .color(MenuHighlightStyle.progressTrack(self.isHighlighted)))

            // Fill
            if fillWidth > 0 {
                let fillRect = CGRect(x: 0, y: 0, width: min(fillWidth, size.width), height: size.height)
                let fillPath = Path { p in p.addRoundedRect(in: fillRect, cornerSize: cornerSize) }
                context.fill(
                    fillPath,
                    with: .color(MenuHighlightStyle.progressTint(self.isHighlighted, fallback: self.tint)))
            }

            for marker in markers {
                let x = size.width * marker.percent / 100
                switch marker.kind {
                case .quotaWarning:
                    let markerRect = Self.warningMarkerRect(x: x, size: size, scale: scale)
                    let markerStripeRect = Self.warningMarkerStripeRect(markerRect, scale: scale)
                    let markerPunchPath = Path { p in
                        p.addRect(Self.extendedMarkerRect(markerRect, size: size))
                    }
                    let markerStripePath = Path { p in
                        p.addRect(Self.extendedMarkerRect(markerStripeRect, size: size))
                    }

                    // Match the pace stripe treatment: punch through the bar, then draw a slimmer neutral stripe.
                    context.blendMode = .destinationOut
                    context.fill(markerPunchPath, with: .color(.white.opacity(Self.stripePunchOpacity)))
                    context.blendMode = .normal
                    context.fill(
                        markerStripePath,
                        with: .color(Self.warningMarkerColor(isHighlighted: self.isHighlighted)))
                case .workdayBoundary:
                    let markerRect = Self.workdayMarkerRect(
                        x: x,
                        size: size,
                        scale: scale,
                        appearance: self.workdayTickAppearance)
                    context.fill(
                        Path(markerRect),
                        with: .color(Self.workdayMarkerColor(
                            isHighlighted: self.isHighlighted,
                            appearance: self.workdayTickAppearance)))
                }
            }

            // Pace tip: punch-out + center stripe drawn within the canvas context using Core Graphics
            // blend modes so no SwiftUI compositing modifier (.blendMode, .compositingGroup) is needed.
            if showTip {
                let isDeficit = self.paceOnTop == false
                let useDeficitRed = isDeficit && self.isHighlighted == false
                let stripeColor: Color = if self.isHighlighted {
                    .white
                } else if useDeficitRed {
                    .red
                } else {
                    .green
                }

                let tipSize = CGSize(width: tipWidth, height: size.height)
                let stripes = Self.paceStripePaths(size: tipSize, scale: scale)
                let shift = CGAffineTransform(translationX: tipOffset, y: 0)

                // Punch out of the accumulated track+fill pixels.
                context.blendMode = .destinationOut
                context.fill(stripes.punched.applying(shift), with: .color(.white.opacity(Self.stripePunchOpacity)))
                context.blendMode = .normal

                context.fill(stripes.center.applying(shift), with: .color(stripeColor))
            }
        }
        .frame(height: 6)
        .accessibilityLabel(self.accessibilityLabel)
        .accessibilityValue(self.markerAccessibilityValue)
    }

    private var markerAccessibilityValue: String {
        var parts = [L("%d percent", Self.displayPercent(self.clamped))]
        let markers = Self.resolvedMarkers(
            warningPercents: self.warningMarkerPercents,
            workdayPercents: self.workdayMarkerPercents,
            workdayAppearance: self.workdayTickAppearance)
        let warnings = markers.filter { $0.kind == .quotaWarning }.map(Self.markerPercentText)
        let workdays = markers.filter { $0.kind == .workdayBoundary }.map(Self.markerPercentText)
        if !warnings.isEmpty {
            parts.append("\(L("quota_warnings_title")): \(warnings.joined(separator: ", "))")
        }
        if !workdays.isEmpty {
            parts.append("\(L("weekly_progress_work_days_title")): \(workdays.joined(separator: ", "))")
        }
        return parts.joined(separator: ". ")
    }

    nonisolated static func resolvedMarkers(
        warningPercents: [Double],
        workdayPercents: [Double],
        workdayAppearance: WorkdayTickAppearance = .subtle) -> [Marker]
    {
        let warnings = Self.normalizedMarkerPercents(warningPercents)
        let workdays = workdayAppearance == .hidden ? [] : Self.normalizedMarkerPercents(workdayPercents)
            .filter { workday in !warnings.contains { abs($0 - workday) < 0.001 } }
        return (
            warnings.map { Marker(percent: $0, kind: .quotaWarning) } +
                workdays.map { Marker(percent: $0, kind: .workdayBoundary) })
            .sorted { lhs, rhs in lhs.percent < rhs.percent }
    }

    private nonisolated static func normalizedMarkerPercents(_ values: [Double]) -> [Double] {
        values
            .map(self.clampedPercent)
            .filter { $0 > 0 && $0 < 100 }
            .reduce(into: [Double]()) { result, value in
                if !result.contains(where: { abs($0 - value) < 0.001 }) {
                    result.append(value)
                }
            }
    }

    private nonisolated static func markerPercentText(_ marker: Marker) -> String {
        "\(Int(marker.percent.rounded()))%"
    }

    /// Aligns edge rendering with the rounded percent label: sub-0.5% is empty and 99.5%+ is full.
    nonisolated static func renderedFillPercent(_ percent: Double) -> Double {
        let clamped = Self.clampedPercent(percent)
        let displayPercent = Self.displayPercent(clamped)
        if displayPercent <= 0 { return 0 }
        if displayPercent >= 100 { return 100 }
        return clamped
    }

    private static func paceStripePaths(size: CGSize, scale: CGFloat) -> (punched: Path, center: Path) {
        let rect = CGRect(origin: .zero, size: size)
        let extend = size.height * 2
        let stripeTopY: CGFloat = -extend
        let stripeBottomY: CGFloat = size.height + extend
        let align: (CGFloat) -> CGFloat = { value in
            (value * scale).rounded() / scale
        }

        let stripeWidth = Self.paceStripeWidth(for: scale)
        let punchWidth = stripeWidth * 3
        let stripeInset = 1 / scale
        let stripeAnchorX = align(rect.maxX - stripeInset)
        let stripeMinY = align(stripeTopY)
        let stripeMaxY = align(stripeBottomY)
        let anchorTopX = stripeAnchorX
        var punchedStripe = Path()
        var centerStripe = Path()
        let availableWidth = (anchorTopX - punchWidth) - rect.minX
        guard availableWidth >= 0 else { return (punchedStripe, centerStripe) }

        let punchRightTopX = align(anchorTopX)
        let punchLeftTopX = punchRightTopX - punchWidth
        let punchRightBottomX = punchRightTopX
        let punchLeftBottomX = punchLeftTopX
        punchedStripe.addPath(Path { path in
            path.move(to: CGPoint(x: punchLeftTopX, y: stripeMinY))
            path.addLine(to: CGPoint(x: punchRightTopX, y: stripeMinY))
            path.addLine(to: CGPoint(x: punchRightBottomX, y: stripeMaxY))
            path.addLine(to: CGPoint(x: punchLeftBottomX, y: stripeMaxY))
            path.closeSubpath()
        })

        let centerLeftTopX = align(punchLeftTopX + (punchWidth - stripeWidth) / 2)
        let centerRightTopX = centerLeftTopX + stripeWidth
        let centerRightBottomX = centerRightTopX
        let centerLeftBottomX = centerLeftTopX
        centerStripe.addPath(Path { path in
            path.move(to: CGPoint(x: centerLeftTopX, y: stripeMinY))
            path.addLine(to: CGPoint(x: centerRightTopX, y: stripeMinY))
            path.addLine(to: CGPoint(x: centerRightBottomX, y: stripeMaxY))
            path.addLine(to: CGPoint(x: centerLeftBottomX, y: stripeMaxY))
            path.closeSubpath()
        })

        return (punchedStripe, centerStripe)
    }

    nonisolated static func warningMarkerRect(x: CGFloat, size: CGSize, scale rawScale: CGFloat) -> CGRect {
        let scale = max(rawScale, 1)
        let width = Self.warningMarkerPunchWidth
        let align: (CGFloat) -> CGFloat = { value in
            (value * scale).rounded() / scale
        }

        return CGRect(
            x: align(x - width / 2),
            y: 0,
            width: width,
            height: align(size.height))
    }

    nonisolated static func warningMarkerStripeRect(_ markerRect: CGRect, scale rawScale: CGFloat) -> CGRect {
        let scale = max(rawScale, 1)
        let width = min(markerRect.width, max(1 / scale, Self.warningMarkerStripeWidth))
        let align: (CGFloat) -> CGFloat = { value in
            (value * scale).rounded() / scale
        }

        return CGRect(
            x: align(markerRect.midX - width / 2),
            y: markerRect.minY,
            width: width,
            height: markerRect.height)
    }

    nonisolated static func workdayMarkerRect(
        x: CGFloat,
        size: CGSize,
        scale rawScale: CGFloat,
        appearance: WorkdayTickAppearance = .subtle) -> CGRect
    {
        let scale = max(rawScale, 1)
        let width: CGFloat
        let height: CGFloat
        switch appearance {
        case .hidden:
            return .zero
        case .subtle:
            width = 1 / scale
            height = max(1 / scale, size.height * 0.5)
        case .highContrast:
            width = max(1 / scale, 1.96)
            height = size.height
        }
        let align: (CGFloat) -> CGFloat = { value in
            (value * scale).rounded() / scale
        }
        return CGRect(
            x: align(x - width / 2),
            y: align(size.height - height),
            width: width,
            height: align(height))
    }

    private nonisolated static func extendedMarkerRect(_ rect: CGRect, size: CGSize) -> CGRect {
        let extend = size.height * 2
        return rect.insetBy(dx: 0, dy: -extend)
    }

    nonisolated static func warningMarkerColor(isHighlighted: Bool) -> Color {
        isHighlighted ? .white.opacity(0.96) : .primary.opacity(0.68)
    }

    nonisolated static func workdayMarkerColor(
        isHighlighted: Bool,
        appearance: WorkdayTickAppearance = .subtle) -> Color
    {
        switch appearance {
        case .hidden:
            .clear
        case .subtle:
            if isHighlighted {
                .white.opacity(0.55)
            } else {
                .primary.opacity(0.30)
            }
        case .highContrast:
            if isHighlighted {
                .white.opacity(0.96)
            } else {
                .primary.opacity(0.85)
            }
        }
    }

    private nonisolated static func displayPercent(_ percent: Double) -> Int {
        Int(self.clampedPercent(percent).rounded())
    }

    private nonisolated static func clampedPercent(_ value: Double?) -> Double {
        guard let value else { return 0 }
        return min(100, max(0, value))
    }
}
