import ForgejoKit
import SwiftUI

struct DiffView: View {
    let diff: ParsedDiff
    var reviewComments: [ReviewComment] = []
    var onLineTap: ((DiffLine, String) -> Void)?

    private var commentIndex: [String: [Int: [ReviewComment]]] {
        Self.buildCommentIndex(reviewComments)
    }

    static func buildCommentIndex(_ comments: [ReviewComment]) -> [String: [Int: [ReviewComment]]] {
        var index: [String: [Int: [ReviewComment]]] = [:]
        for comment in comments {
            let path = comment.path
            let pos = comment.position ?? comment.originalPosition ?? 0
            guard pos > 0 else { continue }
            index[path, default: [:]][pos, default: []].append(comment)
        }
        return index
    }

    var body: some View {
        ScrollView(.horizontal) {
            // `.fixedSize(horizontal:)` gives the content a definite intrinsic
            // width (the widest line) inside the horizontal scroll view. Without
            // it, the rows' `maxWidth: .infinity` resolves ambiguously against the
            // scroll view's unspecified width proposal, so on some devices the
            // content collapses to the viewport width, clipping long lines with
            // nothing to scroll to instead of scrolling horizontally.
            LazyVStack(alignment: .leading, spacing: 0) {
                ForEach(Array(diff.files.enumerated()), id: \.offset) { fileIndex, file in
                    let filePath = file.newName == "/dev/null" ? file.oldName : file.newName

                    HStack(spacing: 4) {
                        Image(systemName: "doc.text")
                            .font(.caption2)
                        Text(filePath)
                            .font(.system(.caption, design: .monospaced))
                            .fontWeight(.semibold)
                            .fixedSize(horizontal: true, vertical: false)
                    }
                    .padding(.horizontal, 10)
                    .padding(.vertical, 8)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .glassEffect(.regular)

                    ForEach(Array(file.hunks.enumerated()), id: \.offset) { _, hunk in
                        ForEach(hunk.lines) { line in
                            diffLineRow(line: line, filePath: filePath)

                            if !reviewComments.isEmpty {
                                ForEach(commentsForLine(line, path: filePath)) { comment in
                                    InlineCommentBubble(comment: comment)
                                }
                            }
                        }
                    }

                    if fileIndex < diff.files.count - 1 {
                        Spacer().frame(height: 8)
                    }
                }
            }
            .fixedSize(horizontal: true, vertical: false)
        }
    }

    @ViewBuilder
    private func diffLineRow(line: DiffLine, filePath: String) -> some View {
        let isCommentable = onLineTap != nil && line.type != .header && line.diffPosition != nil
        let row = HStack(spacing: 0) {
            HStack(spacing: 2) {
                Text(line.oldLineNumber.map { "\($0)" } ?? "")
                    .font(.system(.caption2, design: .monospaced))
                    .foregroundStyle(.tertiary)
                    .frame(minWidth: 20, alignment: .trailing)

                Text(line.newLineNumber.map { "\($0)" } ?? "")
                    .font(.system(.caption2, design: .monospaced))
                    .foregroundStyle(.tertiary)
                    .frame(minWidth: 20, alignment: .trailing)
            }
            .padding(.trailing, 2)

            Rectangle()
                .fill(.quaternary)
                .frame(width: 1)
                .padding(.vertical, 1)

            Text(line.content)
                .font(.system(.caption2, design: .monospaced))
                .foregroundStyle(line.type.color)
                .fixedSize(horizontal: true, vertical: false)
                .padding(.leading, 4)

            Spacer(minLength: 0)

            if isCommentable {
                Image(systemName: "plus.bubble")
                    .font(.caption2)
                    .foregroundStyle(.tertiary)
                    .padding(.trailing, 4)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.horizontal, 2)
        .padding(.vertical, 1)
        .background(line.type.backgroundColor)
        .contentShape(Rectangle())

        // A tappable line is a Button so VoiceOver announces it and can activate it.
        if isCommentable {
            Button {
                onLineTap?(line, filePath)
            } label: {
                row
            }
            .buttonStyle(.plain)
            .accessibilityHint("Add a comment on this line")
        } else {
            row
        }
    }

    private func commentsForLine(_ line: DiffLine, path: String) -> [ReviewComment] {
        guard let linePos = line.diffPosition else { return [] }
        return commentIndex[path]?[linePos] ?? []
    }
}

private struct InlineCommentBubble: View {
    let comment: ReviewComment

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(spacing: 4) {
                Text(comment.user?.login ?? "Unknown")
                    .font(.caption2)
                    .fontWeight(.medium)
                if let date = comment.createdAt {
                    Text(formatRelativeDate(date))
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                }
            }
            MarkdownPreview(text: comment.body)
        }
        .padding(8)
        .frame(maxWidth: .infinity, alignment: .leading)
        .containerRelativeFrame(.horizontal, alignment: .leading) { width, _ in
            max(0, width - 52)
        }
        .background(Color.blue.opacity(0.08))
        .clipShape(RoundedRectangle(cornerRadius: 6))
        .padding(.leading, 48)
        .padding(.trailing, 4)
        .padding(.vertical, 2)
    }
}

#if DEBUG
    #Preview {
        List {
            DiffView(diff: .preview)
                .listRowInsets(EdgeInsets())
        }
    }
#endif
