import AppKit
import CodexBarCore
import SwiftUI

struct StorageBreakdownMenuView: View {
    let footprint: ProviderStorageFootprint
    let width: CGFloat
    let maxHeight: CGFloat
    let onExpansionHeightChange: ((CGFloat) -> Void)?

    @State private var otherExpanded = false

    init(
        footprint: ProviderStorageFootprint,
        width: CGFloat,
        maxHeight: CGFloat = 560,
        onExpansionHeightChange: ((CGFloat) -> Void)? = nil)
    {
        self.footprint = footprint
        self.width = width
        self.maxHeight = maxHeight
        self.onExpansionHeightChange = onExpansionHeightChange
    }

    /// One entry in the segmented bar and its matching legend row. Overflow components past the row
    /// budget collapse into a single trailing "Other" segment with no copyable path of its own.
    private struct Segment: Identifiable {
        let id: String
        let name: String
        let bytes: Int64
        let color: Color
        let path: String?
    }

    /// How many legend rows we let the breakdown show before collapsing the tail into "Other".
    private static let maxRows = 8

    private static let segmentPalette: [Color] = [
        Color(red: 0.20, green: 0.51, blue: 0.96),
        Color(red: 0.96, green: 0.55, blue: 0.20),
        Color(red: 0.30, green: 0.78, blue: 0.47),
        Color(red: 0.66, green: 0.42, blue: 0.93),
        Color(red: 0.95, green: 0.74, blue: 0.22),
        Color(red: 0.92, green: 0.36, blue: 0.55),
        Color(red: 0.27, green: 0.76, blue: 0.82),
    ]

    private static let otherColor = Color(nsColor: .tertiaryLabelColor)
    private static let overflowRowHeight: CGFloat = 18
    private static let overflowRowSpacing: CGFloat = 4
    private static let overflowTopSpacing: CGFloat = 6

    var cleanupRecommendations: [ProviderStorageRecommendation] {
        self.footprint.cleanupRecommendations
    }

    var copyablePaths: [String] {
        let recommendationPaths = self.cleanupRecommendations.map(\.path)
        return self.footprint.components.map(\.path) + recommendationPaths
    }

    /// Visible components mapped to colored segments, with any tail beyond `maxRows` folded into a
    /// single "Other" entry so the bar and legend never exceed the row budget.
    private var segments: [Segment] {
        let components = self.footprint.components
        guard !components.isEmpty else { return [] }

        func color(_ index: Int) -> Color {
            Self.segmentPalette[index % Self.segmentPalette.count]
        }

        func segment(_ component: ProviderStorageFootprint.Component, _ index: Int) -> Segment {
            Segment(
                id: component.id,
                name: component.name,
                bytes: max(component.totalBytes, 0),
                color: color(index),
                path: component.path)
        }

        if components.count <= Self.maxRows {
            return components.enumerated().map { segment($1, $0) }
        }

        let visible = components.prefix(Self.maxRows - 1)
        let overflow = components.dropFirst(Self.maxRows - 1)
        let otherBytes = overflow.reduce(Int64(0)) { partial, component in
            let bytes = max(component.totalBytes, 0)
            let (sum, overflowed) = partial.addingReportingOverflow(bytes)
            return overflowed ? .max : sum
        }
        return visible.enumerated().map { segment($1, $0) } + [
            Segment(
                id: "__other__",
                name: String(format: L("Other (%d items)"), overflow.count),
                bytes: otherBytes,
                color: Self.otherColor,
                path: nil),
        ]
    }

    private var segmentTotalBytes: Double {
        self.segments.reduce(0) { $0 + Double($1.bytes) }
    }

    /// The components folded into the trailing "Other" segment, revealed when it is expanded.
    private var overflowComponents: [ProviderStorageFootprint.Component] {
        let components = self.footprint.components
        guard components.count > Self.maxRows else { return [] }
        return Array(components.dropFirst(Self.maxRows - 1))
    }

    private var overflowExpansionHeight: CGFloat {
        let count = CGFloat(self.overflowComponents.count)
        guard count > 0 else { return 0 }
        return Self.overflowTopSpacing
            + count * Self.overflowRowHeight
            + (count - 1) * Self.overflowRowSpacing
    }

    var body: some View {
        ScrollView(.vertical) {
            self.content
        }
        .scrollIndicators(.visible)
        .frame(
            minWidth: self.width,
            idealWidth: self.width,
            maxWidth: self.width,
            maxHeight: self.maxHeight,
            alignment: .topLeading)
    }

    private var content: some View {
        VStack(alignment: .leading, spacing: 10) {
            VStack(alignment: .leading, spacing: 3) {
                Text(L("Storage"))
                    .font(.body)
                    .fontWeight(.medium)
                Text(String(format: L("Total: %@"), UsageFormatter.byteCountStringLong(self.footprint.totalBytes)))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            if self.segments.isEmpty {
                Text(L("No local data found"))
                    .font(.footnote)
                    .foregroundStyle(.secondary)
            } else {
                self.segmentedBar
                VStack(alignment: .leading, spacing: 6) {
                    ForEach(self.segments) { segment in
                        self.legendRow(segment)
                    }
                }
            }

            if !self.cleanupRecommendations.isEmpty {
                Divider()
                    .padding(.vertical, 2)
                VStack(alignment: .leading, spacing: 8) {
                    Text(L("Cleanup ideas"))
                        .font(.body)
                        .fontWeight(.medium)
                    ForEach(self.cleanupRecommendations) { recommendation in
                        self.recommendationRow(recommendation)
                    }
                }
            }
            if !self.footprint.unreadablePaths.isEmpty {
                Text(String(format: L("%d unreadable item(s) skipped"), self.footprint.unreadablePaths.count))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
        .frame(width: self.width, alignment: .leading)
    }

    private var segmentedBar: some View {
        GeometryReader { proxy in
            ZStack(alignment: .leading) {
                Capsule()
                    .fill(Color(nsColor: .quaternaryLabelColor))
                HStack(spacing: 0) {
                    ForEach(self.segments) { segment in
                        Rectangle()
                            .fill(segment.color)
                            .frame(width: self.segmentWidth(segment, barWidth: proxy.size.width))
                    }
                }
            }
            .clipShape(Capsule())
        }
        .frame(height: 5)
    }

    /// Each segment gets at least `minWidth` so tiny components stay visible, with the remaining width
    /// shared by byte proportion. Reserving the minimums (rather than flooring each width with `max`)
    /// keeps the segments summing to exactly `barWidth`, so none get clipped off the capsule's end.
    private func segmentWidth(_ segment: Segment, barWidth: CGFloat) -> CGFloat {
        let minWidth: CGFloat = 2
        let count = CGFloat(self.segments.count)
        guard self.segmentTotalBytes > 0 else { return barWidth / max(count, 1) }
        let reserved = minWidth * count
        guard barWidth > reserved else { return barWidth / max(count, 1) }
        let remainder = barWidth - reserved
        let proportion = CGFloat(Double(segment.bytes) / self.segmentTotalBytes)
        return minWidth + remainder * proportion
    }

    private func legendRow(_ segment: Segment) -> some View {
        let isOther = segment.path == nil
        return VStack(alignment: .leading, spacing: 6) {
            HStack(spacing: 8) {
                Circle()
                    .fill(segment.color)
                    .frame(width: 9, height: 9)
                Text(segment.name)
                    .font(.caption)
                    .foregroundStyle(.primary)
                    .lineLimit(1)
                    .truncationMode(.middle)
                    .help(segment.path ?? segment.name)
                    .layoutPriority(1)
                Spacer()
                if let path = segment.path {
                    StoragePathCopyButton(path: path)
                } else {
                    self.otherExpandButton
                }
                Text(UsageFormatter.byteCountString(segment.bytes))
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
            }
            if isOther, self.otherExpanded {
                self.overflowList
            }
        }
    }

    private var otherExpandButton: some View {
        Button {
            self.otherExpanded.toggle()
            self.onExpansionHeightChange?(self.otherExpanded ? self.overflowExpansionHeight : 0)
        } label: {
            Image(systemName: self.otherExpanded ? "chevron.down" : "chevron.right")
                .font(.caption2.weight(.semibold))
                .foregroundStyle(.secondary)
                .frame(width: 18, height: 18)
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .help(self.otherExpanded ? L("Collapse") : L("Expand"))
        .accessibilityLabel(self.otherExpanded ? L("Collapse") : L("Expand"))
    }

    /// Plain name + size rows for the items folded into "Other" — no colors, indented under its name.
    private var overflowList: some View {
        VStack(alignment: .leading, spacing: 4) {
            ForEach(self.overflowComponents) { component in
                HStack(spacing: 8) {
                    Text(component.name)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                        .truncationMode(.middle)
                        .help(component.path)
                    Spacer()
                    StoragePathCopyButton(path: component.path)
                    Text(UsageFormatter.byteCountString(component.totalBytes))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                }
            }
        }
        .padding(.leading, 17)
    }

    private func recommendationRow(_ recommendation: ProviderStorageRecommendation) -> some View {
        VStack(alignment: .leading, spacing: 3) {
            HStack(alignment: .firstTextBaseline) {
                Text(L(recommendation.title))
                    .font(.caption)
                    .fontWeight(.medium)
                    .lineLimit(1)
                Spacer()
                Text(UsageFormatter.byteCountString(recommendation.bytes))
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
            }
            HStack(spacing: 4) {
                Text(recommendation.path)
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
                    .truncationMode(.middle)
                    .help(recommendation.path)
                    .layoutPriority(1)
                Spacer()
                StoragePathCopyButton(path: recommendation.path)
            }
            Text(L(recommendation.consequence))
                .font(.caption)
                .foregroundStyle(.secondary)
                .lineLimit(3)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

#if DEBUG
extension StorageBreakdownMenuView {
    var _segmentNamesForTesting: [String] {
        self.segments.map(\.name)
    }

    var _segmentBytesForTesting: [Int64] {
        self.segments.map(\.bytes)
    }

    var _overflowNamesForTesting: [String] {
        self.overflowComponents.map(\.name)
    }

    var _overflowExpansionHeightForTesting: CGFloat {
        self.overflowExpansionHeight
    }

    func _segmentWidthsForTesting(barWidth: CGFloat) -> [CGFloat] {
        self.segments.map { self.segmentWidth($0, barWidth: barWidth) }
    }
}
#endif

struct StoragePathCopyButton: View {
    let path: String

    @State private var didCopy = false
    @State private var resetTask: Task<Void, Never>?

    var body: some View {
        Button {
            self.resetTask?.cancel()
            MenuPasteboardCopy.perform(self.path, completion: {
                self.didCopy = true
                self.resetTask = Task { @MainActor in
                    try? await Task.sleep(for: .seconds(0.9))
                    self.didCopy = false
                }
            })
        } label: {
            Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc")
                .font(.caption2.weight(.semibold))
                .foregroundStyle(.secondary)
                .frame(width: 18, height: 18)
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .help(self.didCopy ? L("Copied") : L("Copy path"))
        .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy path"))
    }
}
