import Observation
import SwiftUI

enum PhoneRoute: Hashable {
    case chat(String)
    case search(String)
    case draftChat
    case draftSearch
    case settings

    static func from(selection: SidebarSelection) -> PhoneRoute {
        switch selection {
        case let .chat(chatID):
            return .chat(chatID)
        case let .search(searchID):
            return .search(searchID)
        case .settings:
            return .settings
        }
    }
}

struct SybilPhoneShellView: View {
    @Bindable var viewModel: SybilViewModel
    @State private var route: PhoneRoute = .draftChat
    @Environment(\.scenePhase) private var scenePhase
    @State private var shouldRefreshOnForeground = false
    @State private var composerFocusRequest = 0
    @State private var phoneStackWidth: CGFloat = BackSwipeMetrics.referenceWidth
    @State private var isSidebarOverlayPresented = false
    @State private var sidebarSwipeOffset: CGFloat = 0
    @State private var sidebarSwipeIsActive = false
    @State private var sidebarSwipeIsCompleting = false
    @State private var sidebarSwipeHasLatched = false
    @State private var sidebarHighlightSelection: SidebarSelection?
    @State private var sidebarHighlightClearTask: Task<Void, Never>?
    @State private var openingSelectionRequestID: UUID?

    private var canRecognizeSidebarSwipe: Bool {
        !isSidebarOverlayPresented && !sidebarSwipeIsCompleting
    }

    private var sidebarOverlayProgress: CGFloat {
        if isSidebarOverlayPresented {
            return 1
        }

        return SidebarOverlaySwipeMetrics.progress(
            for: sidebarSwipeOffset,
            width: phoneStackWidth
        )
    }

    private var shouldRenderSidebarOverlay: Bool {
        isSidebarOverlayPresented ||
            sidebarSwipeIsActive ||
            sidebarSwipeIsCompleting ||
            sidebarOverlayProgress > 0.001
    }

    private var currentRouteSelection: SidebarSelection? {
        switch route {
        case let .chat(chatID):
            return .chat(chatID)
        case let .search(searchID):
            return .search(searchID)
        case .draftChat, .draftSearch, .settings:
            return nil
        }
    }

    private var highlightedSidebarSelection: SidebarSelection? {
        sidebarHighlightSelection ?? currentRouteSelection
    }

    var body: some View {
        GeometryReader { proxy in
            phoneStack(width: proxy.size.width)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
            .onAppear {
                updatePhoneStackWidth(proxy.size.width)
            }
            .onChange(of: proxy.size.width) { _, width in
                updatePhoneStackWidth(width)
            }
        }
        .tint(SybilTheme.primary)
        .animation(.easeOut(duration: 0.22), value: route)
        .animation(.easeOut(duration: 0.18), value: isSidebarOverlayPresented)
        .onChange(of: scenePhase) { _, nextPhase in
            switch nextPhase {
            case .background:
                shouldRefreshOnForeground = true
                viewModel.markAppInactiveForNetwork()
            case .active:
                viewModel.markAppActiveForNetwork()
                guard shouldRefreshOnForeground else {
                    return
                }
                shouldRefreshOnForeground = false
                Task {
                    await viewModel.refreshAfterAppBecameActive(
                        refreshCollections: isSidebarOverlayPresented,
                        refreshSelection: !isSidebarOverlayPresented && viewModel.hasRefreshableSelection
                    )
                }
            case .inactive:
                shouldRefreshOnForeground = true
                viewModel.markAppInactiveForNetwork()
            @unknown default:
                break
            }
        }
    }

    private func phoneStack(width: CGFloat) -> some View {
        ZStack(alignment: .topLeading) {
            phoneWorkspaceLayer
                .zIndex(0)

            phoneSidebarOverlayLayer(width: width)
                .zIndex(1)
        }
    }

    private var phoneWorkspaceLayer: some View {
        SybilPhoneDestinationView(
            viewModel: viewModel,
            composerFocusRequest: $composerFocusRequest,
            route: route,
            onRequestBack: { _ in showSidebarOverlay() },
            onRequestNewChat: sidebarWorkspaceNewChatAction,
            onShowSidebar: showSidebarOverlay
        )
        .background(SybilTheme.background)
        .blur(radius: SidebarOverlaySwipeMetrics.workspaceBlurRadius(for: sidebarOverlayProgress))
        .opacity(SidebarOverlaySwipeMetrics.workspaceOpacity(for: sidebarOverlayProgress))
        .allowsHitTesting(!shouldRenderSidebarOverlay)
        .background {
            sidebarSwipeInstaller
        }
    }

    private func phoneSidebarOverlayLayer(width: CGFloat) -> some View {
        VStack(spacing: 0) {
            phoneOverlayTopBar

            SybilPhoneSidebarRoot(
                viewModel: viewModel,
                highlightedSelection: highlightedSidebarSelection,
                onSelect: openSidebarSelection,
                onRoute: showRouteAndClearSidebarHighlight
            )
        }
        .opacity(sidebarOverlayProgress)
        .blur(radius: SidebarOverlaySwipeMetrics.overlayBlurRadius(for: sidebarOverlayProgress))
        .offset(x: SidebarOverlaySwipeMetrics.overlayOffset(for: sidebarOverlayProgress, width: width))
        .allowsHitTesting(isSidebarOverlayPresented)
        .accessibilityHidden(!isSidebarOverlayPresented)
    }

    private var sidebarSwipeInstaller: some View {
        WorkspaceSwipePanInstaller(
            direction: .right,
            isEnabled: canRecognizeSidebarSwipe,
            onBegan: { width in
                beginSidebarSwipe(containerWidth: width)
            },
            onChanged: { translationX, width in
                updateSidebarSwipe(with: translationX, containerWidth: width)
            },
            onEnded: { translationX, width, velocityX, didFinish in
                finishSidebarSwipe(
                    translationX: translationX,
                    containerWidth: width,
                    velocityX: velocityX,
                    didFinish: didFinish
                )
            }
        )
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private var sidebarWorkspaceNewChatAction: (() -> Void)? {
        guard !isSidebarOverlayPresented else {
            return nil
        }

        return {
            startNewChatFromDestination()
        }
    }

    private var phoneOverlayTopBar: some View {
        HStack(spacing: 12) {
            SybilWordmark(size: 21)
            Spacer()

            Button {
                hideSidebarOverlay()
            } label: {
                Image(systemName: "chevron.right.2")
                    .font(.system(size: 21, weight: .bold))
                    .foregroundStyle(SybilTheme.text)
                    .frame(width: 54, height: 54)
                    .background(
                        Circle()
                            .fill(.ultraThinMaterial)
                            .overlay(
                                Circle()
                                    .fill(SybilTheme.surface.opacity(0.76))
                            )
                    )
                    .overlay(
                        Circle()
                            .stroke(SybilTheme.border.opacity(0.64), lineWidth: 1)
                    )
            }
            .buttonStyle(.plain)
            .accessibilityLabel("Hide conversations")
        }
        .padding(.horizontal, 16)
        .padding(.top, 10)
        .padding(.bottom, 12)
        .background {
            SybilPhoneOverlayBlurBand(edge: .top)
                .ignoresSafeArea(edges: .top)
        }
    }

    private func updatePhoneStackWidth(_ width: CGFloat) {
        phoneStackWidth = max(width, 1)
    }

    private func startNewChatFromDestination() {
        viewModel.startNewChat()
        composerFocusRequest += 1
        showRoute(.draftChat)
    }

    private func showRoute(_ nextRoute: PhoneRoute) {
        let update = {
            route = nextRoute
        }

        if isSidebarOverlayPresented {
            withAnimation(.easeOut(duration: 0.22)) {
                update()
                isSidebarOverlayPresented = false
            }
        } else {
            update()
        }

        resetSidebarSwipe(animated: false)
    }

    private func showRouteAndClearSidebarHighlight(_ nextRoute: PhoneRoute) {
        showRoute(nextRoute)
        clearSidebarHighlight()
    }

    private func showSidebarOverlay() {
        withAnimation(.easeOut(duration: 0.18)) {
            isSidebarOverlayPresented = true
        }
        resetSidebarSwipe(animated: false)
    }

    private func hideSidebarOverlay() {
        withAnimation(.easeOut(duration: 0.18)) {
            isSidebarOverlayPresented = false
        }
        resetSidebarSwipe(animated: false)
    }

    private func openSidebarSelection(_ selection: SidebarSelection) {
        if openingSelectionRequestID != nil, sidebarHighlightSelection == selection {
            return
        }

        let requestID = UUID()
        openingSelectionRequestID = requestID
        setSidebarHighlight(selection)

        Task {
            await viewModel.selectForNavigation(selection)
            guard openingSelectionRequestID == requestID else {
                return
            }

            showRoute(PhoneRoute.from(selection: selection))
            openingSelectionRequestID = nil
            clearSidebarHighlight(selection, after: .milliseconds(260))
        }
    }

    private func setSidebarHighlight(_ selection: SidebarSelection) {
        sidebarHighlightClearTask?.cancel()
        sidebarHighlightSelection = selection
    }

    private func clearSidebarHighlight(_ selection: SidebarSelection, after delay: Duration) {
        sidebarHighlightClearTask?.cancel()
        sidebarHighlightClearTask = Task { @MainActor in
            try? await Task.sleep(for: delay)
            guard !Task.isCancelled,
                  sidebarHighlightSelection == selection,
                  openingSelectionRequestID == nil else {
                return
            }
            sidebarHighlightSelection = nil
        }
    }

    private func clearSidebarHighlight() {
        sidebarHighlightClearTask?.cancel()
        openingSelectionRequestID = nil
        sidebarHighlightSelection = nil
    }

    private func beginSidebarSwipe(containerWidth: CGFloat) {
        let update = {
            phoneStackWidth = max(containerWidth, 1)
            sidebarSwipeIsActive = true
            sidebarSwipeHasLatched = false
        }

        var transaction = Transaction()
        transaction.disablesAnimations = true
        withTransaction(transaction, update)
    }

    private func updateSidebarSwipe(with rawTranslation: CGFloat, containerWidth: CGFloat) {
        let nextOffset = SidebarOverlaySwipeMetrics.clampedOffset(for: rawTranslation, width: containerWidth)
        let nextLatched = SidebarOverlaySwipeMetrics.isLatched(
            offset: nextOffset,
            width: containerWidth,
            isCurrentlyLatched: sidebarSwipeHasLatched
        )

        var transaction = Transaction()
        transaction.disablesAnimations = true
        withTransaction(transaction) {
            phoneStackWidth = max(containerWidth, 1)
            sidebarSwipeOffset = nextOffset
            sidebarSwipeHasLatched = nextLatched
        }
    }

    private func finishSidebarSwipe(
        translationX: CGFloat,
        containerWidth: CGFloat,
        velocityX: CGFloat,
        didFinish: Bool
    ) {
        guard sidebarSwipeIsActive else {
            resetSidebarSwipe(animated: false)
            return
        }

        let finalOffset = SidebarOverlaySwipeMetrics.clampedOffset(for: translationX, width: containerWidth)
        let finalLatched = SidebarOverlaySwipeMetrics.isLatched(
            offset: finalOffset,
            width: containerWidth,
            isCurrentlyLatched: sidebarSwipeHasLatched
        )
        updateSidebarSwipe(with: translationX, containerWidth: containerWidth)

        if didFinish && SidebarOverlaySwipeMetrics.shouldComplete(
            offset: finalOffset,
            velocityX: velocityX,
            width: containerWidth,
            isLatched: finalLatched
        ) {
            completeSidebarSwipe()
            return
        }

        resetSidebarSwipe(animated: true, velocityX: velocityX)
    }

    private func completeSidebarSwipe() {
        guard !sidebarSwipeIsCompleting else {
            return
        }

        sidebarSwipeIsCompleting = true
        withAnimation(.easeOut(duration: 0.18)) {
            isSidebarOverlayPresented = true
        }
        resetSidebarSwipe(animated: false)
    }

    private func resetSidebarSwipe(animated: Bool, velocityX: CGFloat = 0) {
        let currentOffset = sidebarSwipeOffset
        let reset = {
            sidebarSwipeOffset = 0
            sidebarSwipeIsActive = false
            sidebarSwipeIsCompleting = false
            sidebarSwipeHasLatched = false
        }

        if animated {
            withAnimation(
                SidebarOverlaySwipeMetrics.springAnimation(
                    currentOffset: currentOffset,
                    targetOffset: 0,
                    velocityX: velocityX
                )
            ) {
                reset()
            }
        } else {
            reset()
        }
    }
}

private enum SidebarOverlaySwipeMetrics {
    static func clampedOffset(for rawTranslation: CGFloat, width: CGFloat) -> CGFloat {
        BackSwipeMetrics.clampedOffset(for: rawTranslation, width: width)
    }

    static func progress(for offset: CGFloat, width: CGFloat) -> CGFloat {
        BackSwipeMetrics.progress(for: offset, width: width)
    }

    static func isLatched(offset: CGFloat, width: CGFloat, isCurrentlyLatched: Bool = false) -> Bool {
        BackSwipeMetrics.isLatched(offset: offset, width: width, isCurrentlyLatched: isCurrentlyLatched)
    }

    static func shouldComplete(offset: CGFloat, velocityX: CGFloat, width: CGFloat, isLatched: Bool) -> Bool {
        BackSwipeMetrics.shouldComplete(offset: offset, velocityX: velocityX, width: width, isLatched: isLatched)
    }

    static func springAnimation(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Animation {
        BackSwipeMetrics.springAnimation(currentOffset: currentOffset, targetOffset: targetOffset, velocityX: velocityX)
    }

    static func overlayOffset(for progress: CGFloat, width: CGFloat) -> CGFloat {
        -(1 - min(max(progress, 0), 1)) * min(max(width * 0.18, 44), 76)
    }

    static func overlayBlurRadius(for progress: CGFloat) -> CGFloat {
        (1 - min(max(progress, 0), 1)) * 18
    }

    static func workspaceBlurRadius(for progress: CGFloat) -> CGFloat {
        min(max(progress, 0), 1) * 14
    }

    static func workspaceOpacity(for progress: CGFloat) -> CGFloat {
        1 - (min(max(progress, 0), 1) * 0.22)
    }
}

private struct SybilPhoneOverlayBlurBand: View {
    var edge: VerticalEdge

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.ultraThinMaterial)
                .opacity(0.34)

            Rectangle()
                .fill(
                    LinearGradient(
                        colors: gradientColors,
                        startPoint: edge == .top ? .top : .bottom,
                        endPoint: edge == .top ? .bottom : .top
                    )
                )
        }
    }

    private var gradientColors: [Color] {
        [
            Color.black.opacity(0.94),
            SybilTheme.background.opacity(0.78),
            Color.black.opacity(0)
        ]
    }
}

private struct SybilPhoneSidebarRoot: View {
    @Bindable var viewModel: SybilViewModel
    var highlightedSelection: SidebarSelection?
    var onSelect: (SidebarSelection) -> Void
    var onRoute: (PhoneRoute) -> Void

    var body: some View {
        VStack(spacing: 0) {
            if let errorMessage = viewModel.errorMessage {
                Text(errorMessage)
                    .font(.sybil(.footnote))
                    .foregroundStyle(SybilTheme.danger)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 10)

                Divider()
                    .overlay(SybilTheme.border)
            }

            SybilSidebarItemList(
                viewModel: viewModel,
                isSelected: { item in
                    highlightedSelection == item.selection
                },
                onSelect: { item in
                    onSelect(item.selection)
                }
            )
        }
        .background(SybilTheme.panelGradient)
        .safeAreaInset(edge: .bottom, spacing: 0) {
            bottomToolbar
        }
    }

    private var bottomToolbar: some View {
        VStack(spacing: 0) {
            Divider()
                .overlay(SybilTheme.border)

            HStack(spacing: 12) {
                toolbarIconButton(systemImage: "gearshape", accessibilityLabel: "Settings") {
                    viewModel.openSettings()
                    onRoute(.settings)
                }

                Spacer()

                toolbarIconButton(systemImage: "magnifyingglass", accessibilityLabel: "New search") {
                    viewModel.startNewSearch()
                    onRoute(.draftSearch)
                }

                toolbarIconButton(systemImage: "plus", accessibilityLabel: "New chat", isPrimary: true) {
                    viewModel.startNewChat()
                    onRoute(.draftChat)
                }
            }
            .padding(.horizontal, 18)
            .padding(.vertical, 10)
            .background(SybilTheme.panelGradient)
        }
    }

    private func toolbarIconButton(
        systemImage: String,
        accessibilityLabel: String,
        isPrimary: Bool = false,
        action: @escaping () -> Void
    ) -> some View {
        Button(action: action) {
            Image(systemName: systemImage)
                .font(.system(size: 18, weight: .semibold))
                .foregroundStyle(isPrimary ? SybilTheme.text : SybilTheme.textMuted)
                .frame(width: 42, height: 42)
                .background(
                    Circle()
                        .fill(
                            isPrimary
                                ? AnyShapeStyle(SybilTheme.primaryGradient)
                                : AnyShapeStyle(SybilTheme.surface.opacity(0.78))
                        )
                )
                .overlay(
                    Circle()
                        .stroke(isPrimary ? SybilTheme.primary.opacity(0.42) : SybilTheme.border.opacity(0.76), lineWidth: 1)
                )
        }
        .buttonStyle(.plain)
        .accessibilityLabel(accessibilityLabel)
    }
}

private struct SybilPhoneDestinationView: View {
    @Bindable var viewModel: SybilViewModel
    @Binding var composerFocusRequest: Int
    let route: PhoneRoute
    let onRequestBack: (_ animateNavigation: Bool) -> Void
    let onRequestNewChat: (() -> Void)?
    let onShowSidebar: () -> Void

    var body: some View {
        SybilWorkspaceView(
            viewModel: viewModel,
            composerFocusRequest: composerFocusRequest,
            navigationLeadingControl: .showSidebar,
            onShowSidebar: onShowSidebar,
            onRequestBack: onRequestBack,
            onRequestNewChat: onRequestNewChat
        )
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .task(id: route) {
            applyRoute()
        }
    }

    private func applyRoute() {
        switch route {
        case let .chat(chatID):
            guard viewModel.draftKind != nil || viewModel.selectedItem != .chat(chatID) else {
                return
            }
            viewModel.select(.chat(chatID))
        case let .search(searchID):
            guard viewModel.draftKind != nil || viewModel.selectedItem != .search(searchID) else {
                return
            }
            viewModel.select(.search(searchID))
        case .draftChat:
            viewModel.startNewChat()
        case .draftSearch:
            viewModel.startNewSearch()
        case .settings:
            viewModel.openSettings()
        }
    }
}
