import MarkdownUI
import SwiftUI
import UIKit

struct SybilChatTranscriptView: View {
    var messages: [Message]
    var isLoading: Bool
    var isSending: Bool
    var streamingAssistantPresentation: StreamingAssistantPresentation?
    var topContentInset: CGFloat = 0
    var bottomContentInset: CGFloat = 0
    var bottomPinRequestID: Int = 0
    var isForking: Bool = false
    var onForkResponse: ((String) -> Void)? = nil

    @State private var hasTrackedToolCallMessages = false
    @State private var knownToolCallMessageIDs: Set<String> = []

    private let bottomAnchorID = "sybil-chat-transcript-bottom-anchor"
    private var renderItems: [TranscriptRenderItem] {
        buildTranscriptRenderItems(from: messages)
    }
    private var toolCallMessageIDs: Set<String> {
        Set(messages.compactMap { $0.toolCallMetadata == nil ? nil : $0.id })
    }
    private var enteringToolCallMessageIDs: Set<String> {
        guard hasTrackedToolCallMessages else { return [] }
        return toolCallMessageIDs.subtracting(knownToolCallMessageIDs)
    }
    private var toolCallMessageIDSignature: String {
        toolCallMessageIDs.sorted().joined(separator: "|")
    }

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 26) {
                    if isLoading && messages.isEmpty {
                        Text("Loading messages…")
                            .font(.sybil(.footnote))
                            .foregroundStyle(SybilTheme.textMuted)
                            .padding(.top, 24)
                    }

                    ForEach(renderItems) { item in
                        switch item {
                        case let .message(message):
                            MessageBubble(
                                message: message,
                                isSending: isSending,
                                streamingAssistantPresentation: streamingAssistantPresentation,
                                isForking: isForking,
                                onFork: {
                                    onForkResponse?(message.id)
                                }
                            )
                                .frame(maxWidth: .infinity)
                        case let .toolGroup(id, messages):
                            ToolCallStackView(
                                groupID: id,
                                messages: messages,
                                entryAnimationIDs: enteringToolCallMessageIDs
                            )
                                .frame(maxWidth: .infinity)
                                .id(id)
                        }
                    }

                    Color.clear
                        .frame(height: 18 + bottomContentInset)
                        .id(bottomAnchorID)
                }
                .frame(maxWidth: SybilLayout.webContentMaxWidth, alignment: .leading)
                .frame(maxWidth: .infinity, alignment: .center)
                .padding(.horizontal, 14)
                .padding(.top, 18 + topContentInset)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .scrollDismissesKeyboard(.interactively)
            .onAppear {
                syncKnownToolCallMessageIDs()
            }
            .onChange(of: toolCallMessageIDSignature) { _, _ in
                syncKnownToolCallMessageIDs()
            }
            .onChange(of: bottomPinRequestID) { _, _ in
                scrollToBottom(with: proxy, animated: true)
            }
        }
    }

    private func scrollToBottom(with proxy: ScrollViewProxy, animated: Bool) {
        let action = {
            proxy.scrollTo(bottomAnchorID, anchor: .bottom)
        }

        if animated {
            withAnimation(.easeOut(duration: 0.18), action)
        } else {
            action()
        }
    }

    private func syncKnownToolCallMessageIDs() {
        guard !toolCallMessageIDs.isEmpty else { return }
        knownToolCallMessageIDs.formUnion(toolCallMessageIDs)
        hasTrackedToolCallMessages = true
    }
}

enum TranscriptRenderItem: Identifiable {
    case message(Message)
    case toolGroup(id: String, messages: [Message])

    var id: String {
        switch self {
        case let .message(message):
            return message.id
        case let .toolGroup(id, _):
            return "tool-group-\(id)"
        }
    }
}

func buildTranscriptRenderItems(from messages: [Message]) -> [TranscriptRenderItem] {
    var items: [TranscriptRenderItem] = []
    var toolRun: [Message] = []

    func flushToolRun() {
        guard !toolRun.isEmpty else { return }
        if toolRun.count == 1, let message = toolRun.first {
            items.append(.message(message))
        } else if let first = toolRun.first {
            items.append(.toolGroup(id: first.id, messages: toolRun))
        }
        toolRun.removeAll(keepingCapacity: true)
    }

    for message in messages {
        if message.toolCallMetadata != nil {
            toolRun.append(message)
        } else {
            flushToolRun()
            items.append(.message(message))
        }
    }

    flushToolRun()
    return items
}

private struct MessageBubble: View {
    var message: Message
    var isSending: Bool
    var streamingAssistantPresentation: StreamingAssistantPresentation?
    var isForking: Bool
    var onFork: (() -> Void)?

    private var toolCallMetadata: ToolCallMetadata? {
        message.toolCallMetadata
    }

    private var isUser: Bool {
        message.role == .user
    }

    private var isStreamingAssistant: Bool {
        message.role == .assistant && message.id.hasPrefix("temp-assistant-")
    }

    private var matchingStreamingAssistantPresentation: StreamingAssistantPresentation? {
        guard streamingAssistantPresentation?.messageID == message.id else { return nil }
        return streamingAssistantPresentation
    }

    private var canFork: Bool {
        message.role == .assistant &&
            !message.id.hasPrefix("temp-") &&
            onFork != nil
    }

    var body: some View {
        Group {
            if canFork {
                bubbleContent
                    .contextMenu {
                        Button {
                            UIPasteboard.general.string = message.content
                        } label: {
                            Label("Copy", systemImage: "doc.on.doc")
                        }

                        Button {
                            onFork?()
                        } label: {
                            Label("Fork", systemImage: "arrow.triangle.branch")
                        }
                        .disabled(isForking)
                    }
            } else {
                bubbleContent
            }
        }
    }

    private var bubbleContent: some View {
        HStack(alignment: .top, spacing: 0) {
            leadingSpacer

            if let toolCallMetadata {
                ToolCallActivityChip(
                    metadata: toolCallMetadata,
                    fallbackContent: message.content,
                    createdAt: message.createdAt
                )
            } else {
                VStack(alignment: .leading, spacing: 8) {
                    if !message.attachments.isEmpty {
                        SybilAttachmentListView(
                            attachments: message.attachments,
                            tone: isUser ? .user : .assistant
                        )
                    }

                    if isStreamingAssistant {
                        StreamingAssistantContentView(
                            content: message.content,
                            isSending: isSending,
                            presentation: matchingStreamingAssistantPresentation
                        )
                    } else if !message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                        Markdown(message.content)
                            .tint(SybilTheme.primary)
                            .foregroundStyle(isUser ? SybilTheme.text : SybilTheme.text.opacity(0.95))
                            .markdownTheme(.sybilReadable)
                    }
                }
                .padding(.horizontal, isUser ? 14 : 2)
                .padding(.vertical, isUser ? 13 : 2)
                .textSelection(.enabled)
                .background(
                    Group {
                        if isUser {
                            RoundedRectangle(cornerRadius: 16)
                                .fill(SybilTheme.userBubbleGradient)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 16)
                                        .stroke(SybilTheme.primary.opacity(0.45), lineWidth: 1)
                                )
                        } else {
                            RoundedRectangle(cornerRadius: 0)
                                .fill(Color.clear)
                        }
                    }
                )
                .frame(maxWidth: isUser ? 420 : nil, alignment: isUser ? .trailing : .leading)
            }

            trailingSpacer
        }
        .frame(maxWidth: .infinity, alignment: isUser ? .trailing : .leading)
    }

    @ViewBuilder
    private var leadingSpacer: some View {
        if isUser {
            Spacer(minLength: 44)
        }
    }

    @ViewBuilder
    private var trailingSpacer: some View {
        if !isUser {
            Spacer(minLength: 0)
        }
    }
}

private struct StreamingTraceHeightPreferenceKey: PreferenceKey {
    static let defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
    }
}

struct StreamingAssistantContentView: View {
    private enum Phase: Hashable {
        case pending
        case trace(Int)
        case answer
    }

    var content: String
    var isSending: Bool
    var presentation: StreamingAssistantPresentation?

    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var retainedTraceHeight: CGFloat = 0

    private var traceText: String? {
        guard let trace = presentation?.ephemeralTrace?.trimmingCharacters(in: .whitespacesAndNewlines),
              !trace.isEmpty
        else {
            return nil
        }
        return trace
    }

    private var hasAnswer: Bool {
        !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    private var phase: Phase {
        if hasAnswer {
            return .answer
        }
        if traceText != nil {
            return .trace(presentation?.traceRevision ?? 0)
        }
        return .pending
    }

    private var traceTransition: AnyTransition {
        guard !reduceMotion else { return .identity }
        return .asymmetric(
            insertion: .opacity.combined(with: .offset(y: 5)),
            removal: .opacity.combined(with: .offset(y: -5))
        )
    }

    private var answerTransition: AnyTransition {
        guard !reduceMotion else { return .identity }
        return .opacity.combined(with: .offset(y: 3))
    }

    var body: some View {
        ZStack(alignment: .topLeading) {
            if let traceText {
                Text(traceText)
                    .font(.sybil(size: 15))
                    .italic()
                    .foregroundStyle(SybilTheme.textMuted.opacity(0.88))
                    .lineSpacing(5)
                    .fixedSize(horizontal: false, vertical: true)
                    .opacity(0.78)
                    .textSelection(.disabled)
                    .allowsHitTesting(false)
                    .accessibilityLabel("Assistant is working")
                    .background {
                        GeometryReader { geometry in
                            Color.clear.preference(
                                key: StreamingTraceHeightPreferenceKey.self,
                                value: geometry.size.height
                            )
                        }
                    }
                    .id("thinking-trace-\(presentation?.traceRevision ?? 0)")
                    .transition(traceTransition)
                    .zIndex(1)
            }

            if hasAnswer {
                Markdown(content)
                    .tint(SybilTheme.primary)
                    .foregroundStyle(SybilTheme.text.opacity(0.95))
                    .markdownTheme(.sybilReadable)
                    .textSelection(.enabled)
                    .id("streaming-assistant-answer")
                    .transition(answerTransition)
                    .zIndex(2)
            }

            if isSending && !hasAnswer && traceText == nil {
                HStack(spacing: 8) {
                    ProgressView()
                        .controlSize(.small)
                        .tint(SybilTheme.primary)
                    Text("Thinking…")
                        .font(.sybil(.footnote))
                        .foregroundStyle(SybilTheme.textMuted)
                }
                .padding(.vertical, 2)
                .transition(reduceMotion ? .identity : .opacity)
            }
        }
        .frame(maxWidth: .infinity, minHeight: max(24, retainedTraceHeight), alignment: .topLeading)
        .onPreferenceChange(StreamingTraceHeightPreferenceKey.self) { measuredHeight in
            guard measuredHeight.isFinite, measuredHeight > retainedTraceHeight else { return }
            retainedTraceHeight = measuredHeight
        }
        .animation(reduceMotion ? nil : .easeOut(duration: 0.20), value: phase)
    }
}

private struct ToolCallStackView: View {
    private struct CardLayout {
        var x: CGFloat
        var y: CGFloat
        var scale: CGFloat
        var opacity: Double
        var zIndex: Double
    }

    var groupID: String
    var messages: [Message]
    var entryAnimationIDs: Set<String>

    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var isExpanded = false

    private let visibleCollapsedLimit = 4
    private let cardHeight: CGFloat = 62
    private let expandedGap: CGFloat = 10
    private let collapsedStepX: CGFloat = 11
    private let collapsedStepY: CGFloat = 10
    private let toggleSize: CGFloat = 32
    private let toggleGap: CGFloat = 12

    private var animation: Animation? {
        reduceMotion ? nil : .easeInOut(duration: 0.34)
    }

    private var visibleCollapsedCount: Int {
        min(messages.count, visibleCollapsedLimit)
    }

    private var hiddenCount: Int {
        max(0, messages.count - visibleCollapsedLimit)
    }

    private var containerHeight: CGFloat {
        if isExpanded {
            return cardHeight + CGFloat(max(0, messages.count - 1)) * (cardHeight + expandedGap)
        }
        return cardHeight + CGFloat(max(0, visibleCollapsedCount - 1)) * collapsedStepY
    }

    private var accessibilityLabel: String {
        "\(messages.count) tool \(messages.count == 1 ? "call" : "calls")"
    }

    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            GeometryReader { geometry in
                let cardWidth = max(220, min(520, geometry.size.width - toggleSize - toggleGap))
                let toggleX = cardWidth + toggleGap

                ZStack(alignment: .topLeading) {
                    ForEach(Array(messages.enumerated()), id: \.element.id) { index, message in
                        let layout = layout(for: index)
                        let depth = messages.count - index - 1
                        let isHidden = !isExpanded && depth >= visibleCollapsedLimit
                        let shouldAnimateEntry = entryAnimationIDs.contains(message.id) && !isHidden

                        ToolCallStackCard(
                            message: message,
                            cardHeight: cardHeight,
                            compactLayout: true,
                            animateEntry: shouldAnimateEntry
                        )
                            .frame(width: cardWidth, height: cardHeight, alignment: .topLeading)
                            .scaleEffect(layout.scale, anchor: .topLeading)
                            .opacity(layout.opacity)
                            .offset(x: layout.x, y: layout.y)
                            .zIndex(layout.zIndex)
                            .allowsHitTesting(!isHidden)
                            .accessibilityHidden(isHidden)
                    }

                    if !isExpanded && hiddenCount > 0 {
                        Text("+\(hiddenCount)")
                            .font(.sybil(.caption2, weight: .semibold))
                            .foregroundStyle(SybilTheme.accent.opacity(0.95))
                            .padding(.horizontal, 7)
                            .padding(.vertical, 3)
                            .background(
                                Capsule()
                                    .fill(Color.black.opacity(0.58))
                                    .overlay(
                                        Capsule()
                                            .stroke(SybilTheme.accent.opacity(0.34), lineWidth: 1)
                                    )
                            )
                            .offset(x: max(0, cardWidth - 56), y: containerHeight - 13)
                            .transition(.opacity)
                    }

                    Button {
                        withAnimation(animation) {
                            isExpanded.toggle()
                        }
                    } label: {
                        Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
                            .font(.system(size: 14, weight: .bold))
                            .foregroundStyle(SybilTheme.accent.opacity(0.95))
                            .frame(width: toggleSize, height: toggleSize)
                            .background(
                                Circle()
                                    .fill(
                                        LinearGradient(
                                            colors: [
                                                Color(red: 0.06, green: 0.08, blue: 0.15).opacity(0.96),
                                                Color(red: 0.03, green: 0.04, blue: 0.10).opacity(0.96)
                                            ],
                                            startPoint: .top,
                                            endPoint: .bottom
                                        )
                                    )
                                    .overlay(
                                        Circle()
                                            .stroke(SybilTheme.accent.opacity(0.38), lineWidth: 1)
                                    )
                                    .shadow(color: Color.black.opacity(0.30), radius: 10, x: 0, y: 6)
                            )
                    }
                    .buttonStyle(.plain)
                    .accessibilityLabel("\(isExpanded ? "Collapse" : "Expand") \(accessibilityLabel)")
                    .offset(x: toggleX, y: 8)
                    .zIndex(Double(messages.count + 2))
                }
                .frame(width: cardWidth + toggleSize + toggleGap, height: containerHeight, alignment: .topLeading)
                .animation(animation, value: isExpanded)
            }
            .frame(height: containerHeight)

            Spacer(minLength: 0)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func layout(for index: Int) -> CardLayout {
        if isExpanded {
            return CardLayout(
                x: 0,
                y: CGFloat(index) * (cardHeight + expandedGap),
                scale: 1,
                opacity: 1,
                zIndex: Double(messages.count - index)
            )
        }

        let depth = messages.count - index - 1
        let visibleDepth = min(depth, visibleCollapsedLimit - 1)
        let isHidden = depth >= visibleCollapsedLimit
        return CardLayout(
            x: CGFloat(visibleDepth) * collapsedStepX,
            y: CGFloat(visibleDepth) * collapsedStepY,
            scale: max(0.88, 1 - CGFloat(visibleDepth) * 0.035),
            opacity: isHidden ? 0 : max(0.34, 1 - Double(visibleDepth) * 0.22),
            zIndex: isHidden ? 0 : Double(visibleCollapsedCount - visibleDepth)
        )
    }
}

private struct ToolCallStackCard: View {
    var message: Message
    var cardHeight: CGFloat
    var compactLayout: Bool
    var animateEntry: Bool

    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var entryAnimationArmed = false
    @State private var didEnter = false

    private var isPreparingEntry: Bool {
        (animateEntry || entryAnimationArmed) && !didEnter
    }

    var body: some View {
        Group {
            if let metadata = message.toolCallMetadata {
                ToolCallActivityChip(
                    metadata: metadata,
                    fallbackContent: message.content,
                    createdAt: message.createdAt,
                    compactLayout: compactLayout
                )
            }
        }
            .frame(height: cardHeight, alignment: .top)
            .scaleEffect(isPreparingEntry ? 1.025 : 1, anchor: .topLeading)
            .offset(y: isPreparingEntry ? -8 : 0)
            .rotation3DEffect(.degrees(isPreparingEntry ? 3 : 0), axis: (x: 1, y: 0, z: 0), anchor: .top)
            .opacity(isPreparingEntry ? 0.72 : 1)
            .onAppear {
                guard !didEnter, !entryAnimationArmed else { return }
                guard animateEntry else {
                    didEnter = true
                    return
                }
                entryAnimationArmed = true
                if reduceMotion {
                    didEnter = true
                } else {
                    withAnimation(.easeOut(duration: 0.32).delay(0.03)) {
                        didEnter = true
                    }
                }
            }
    }
}

private struct ToolCallActivityChip: View {
    enum VisualState {
        case initiated
        case completed
        case failed
    }

    var metadata: ToolCallMetadata
    var fallbackContent: String
    var createdAt: Date
    var compactLayout: Bool = false

    private var summary: String {
        if let text = metadata.summary?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty {
            return text
        }
        if (metadata.status ?? "").lowercased() == "failed",
           let error = metadata.error?.trimmingCharacters(in: .whitespacesAndNewlines),
           !error.isEmpty {
            return "Tool failed: \(error)"
        }
        if let preview = metadata.resultPreview?.trimmingCharacters(in: .whitespacesAndNewlines), !preview.isEmpty {
            return preview
        }
        if !fallbackContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return fallbackContent
        }
        return "Ran tool '\(metadata.toolName ?? "unknown_tool")'."
    }

    private var toolLabel: String {
        let raw = metadata.toolName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !raw.isEmpty else { return "Tool call" }
        return raw
            .replacingOccurrences(of: "_", with: " ")
            .split(separator: " ")
            .map { word in
                word.prefix(1).uppercased() + word.dropFirst()
            }
            .joined(separator: " ")
    }

    private var iconName: String {
        let name = (metadata.toolName ?? "").lowercased()
        if name.contains("search") {
            return "globe"
        }
        if name.contains("url") || name.contains("fetch") || name.contains("http") {
            return "link"
        }
        return "wrench.and.screwdriver"
    }

    private var isFailed: Bool {
        visualState == .failed
    }

    private var visualState: VisualState {
        switch (metadata.status ?? "").lowercased() {
        case "failed":
            return .failed
        case "initiated":
            return .initiated
        default:
            return .completed
        }
    }

    private var detailLabel: String {
        var pieces: [String] = [stateLabel]
        if let durationMs = metadata.durationMs, durationMs > 0 {
            pieces.append("\(durationMs) ms")
        }
        pieces.append(createdAt.sybilShortTimeLabel)
        return pieces.joined(separator: " • ")
    }

    var body: some View {
        HStack(alignment: .top, spacing: 11) {
            ZStack {
                RoundedRectangle(cornerRadius: 9)
                    .fill(iconColor.opacity(0.13))
                    .overlay(
                        RoundedRectangle(cornerRadius: 9)
                            .stroke(iconColor.opacity(0.34), lineWidth: 1)
                    )
                Image(systemName: iconName)
                    .font(.system(size: 14, weight: .semibold))
                    .foregroundStyle(iconColor)
            }
            .frame(width: 30, height: 30)

            VStack(alignment: .leading, spacing: 4) {
                Text(summary)
                    .font(.sybil(.subheadline))
                    .foregroundStyle(isFailed ? SybilTheme.danger.opacity(0.96) : SybilTheme.text.opacity(0.94))
                    .lineSpacing(3)
                    .lineLimit(compactLayout ? 1 : nil)
                    .truncationMode(.tail)
                    .fixedSize(horizontal: false, vertical: !compactLayout)

                HStack(spacing: 6) {
                    Text(toolLabel)
                        .font(.sybil(.caption2, weight: .semibold))
                        .foregroundStyle(iconColor.opacity(0.90))
                        .lineLimit(1)

                    Text(detailLabel)
                        .font(.sybil(.caption2))
                        .foregroundStyle(SybilTheme.textMuted)
                        .lineLimit(1)
                }
            }
        }
        .textSelection(.enabled)
        .padding(.horizontal, 12)
        .padding(.vertical, 10)
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(backgroundGradient)
                .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .stroke(iconColor.opacity(0.34), lineWidth: 1)
                )
        )
        .frame(maxWidth: 520, alignment: .leading)
    }

    private var stateLabel: String {
        switch visualState {
        case .failed:
            return "Failed"
        case .initiated:
            return "Running"
        case .completed:
            return "Completed"
        }
    }

    private var iconColor: Color {
        switch visualState {
        case .failed:
            return SybilTheme.danger
        case .initiated:
            return SybilTheme.warning
        case .completed:
            return SybilTheme.accent
        }
    }

    private var backgroundGradient: LinearGradient {
        switch visualState {
        case .failed:
            return SybilTheme.failedToolCallGradient
        case .initiated:
            return SybilTheme.runningToolCallGradient
        case .completed:
            return SybilTheme.toolCallGradient
        }
    }
}
