import APIClient
import ComponentLibrary
import ComposableArchitecture
import FeatureClipDetail
import Localization
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct ClipEditsListItem {
    @Reducer(state: .equatable)
    public enum Destination {
        case songActions(SongActions)
    }

    @ObservableState
    public struct State: Identifiable, Equatable {
        public var id: Clip.ID { clip.id }
        public var clip: Clip
        @Shared var me: Me
        public let editNumber: Int
        public var hasPlayedForFirstTime: Bool
        public var isCurrentlyEditing: Bool
        public var rootClipDuration: Double

        public var fallbackTotalTimeSeconds: Double = 120
        // Used to show a spinner on the Select button when
        // we're waiting for the full clip to load. Once it's ready,
        // we'll hide the spinner and dismiss the ClipEdits screen.
        public var isLoadingFullClip: Bool = false

        // Extend offset and total duration that accounts
        // for all the nested clips in the history.
        // Example, if Clip B extends Clip A, and Clip C extends Clip B,
        // then:
        // - Clip C Total Duration = Clip A Duration + Clip B Duration
        // - Clip C Extend Offset = Clip A Extend Time + Clip B Extend Time
        // This calculation is done on the backend, and is passed in here.
        public var extendOffset: Double?
        public var totalDuration: Double?

        @Shared(.inMemory(.playingClipKey)) var playingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var isPlaying: Bool = false

        @Presents public var destination: Destination.State?

        public init(
            clip: Clip, me: Shared<Me>,
            editNumber: Int,
            hasPlayedForFirstTime: Bool,
            rootClipDuration: Double,
            isCurrentlyEditing: Bool = false,
            extendOffset: Double? = nil,
            totalDuration: Double? = nil
        ) {
            self.clip = clip
            self._me = me
            self.editNumber = editNumber
            self.hasPlayedForFirstTime = hasPlayedForFirstTime
            self.isCurrentlyEditing = isCurrentlyEditing
            self.rootClipDuration = rootClipDuration
            self.extendOffset = extendOffset
            self.totalDuration = totalDuration
        }

        public var isReadyToPlay: Bool {
            clip.status == .complete || clip.status == .streaming
        }

        // "Extension #4, Part #3, etc."
        public var label: String {
            "\(L10n.FeatureEditClip.extension) #\(editNumber)"
        }

        public var progress: Double = 0.0

        public var offsetProgress: Double {
            // Fallback to rootClipContinueAt if extendOffset is not available
            let offset = extendOffset ?? clip.history?.rootClipContinueAt ?? 0
            return progress + offset
        }

        public var totalTime: Double {
            // Fallback to rootClipDuration if totalDuration is not available
            // This is merely a visual fallback, as the displayed time here
            // won't make necessarily match the actual time.
            return totalDuration ?? rootClipDuration + fallbackTotalTimeSeconds
        }

        public var highlightedPortionStart: Double {
            guard let history = clip.history, !history.clips.isEmpty else { return 0.0 }
            return history.clips.last?.continueAt ?? rootClipDuration
        }

        public var highlightedPortionEnd: Double {
            // Until we add Replace Section, or Edit modes where we can
            // move the right handle, we'll just use the total duration.
            return totalDuration ?? rootClipDuration + fallbackTotalTimeSeconds
        }
    }

    public enum Action {
        case destination(PresentationAction<Destination.Action>)
        case playPauseTapped
        case selectTapped
        case moreTapped
        case likeTapped
        case dislikeTapped
        case updateProgress(Double)
        case setCurrentlyEditing(Bool)
        case setLoadingFullClip(Bool)
        case delegate(Delegate)
        case `internal`(Internal)

        public enum Delegate {
            case togglePlayPause(Clip)
            case didSelectClip(Clip)
            case moreTapped
            case loadClipIntoEditor(Clip)
            case deleteClip(Clip)
        }

        public enum Internal {
            case likeResponse((Bool, Bool), Result<Void, Error>)
            case dislikeResponse((Bool, Bool), Result<Void, Error>)
        }
    }

    public init() {}

    @Dependency(APIClient.self) var apiClient
    @Dependency(APIClientV2.self) var apiClientV2
    @Dependency(\.telemetryClient) var telemetry

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .selectTapped:
                return .send(.delegate(.didSelectClip(state.clip)))

            case .moreTapped:
                state.destination = .songActions(SongActions.State(clip: state.clip, playlist: nil, me: state.$me))
                return .none

            case .likeTapped:
                let originalClip = state.clip
                state.clip.isLiked.toggle()
                return .run { [clip = state.clip] send in
                    await send(.internal(.likeResponse((originalClip.isLiked, originalClip.isDisliked), .init(catching: { try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil) }))))
                }

            case .dislikeTapped:
                let originalClip = state.clip
                state.clip.isDisliked.toggle()
                return .run { [clip = state.clip] send in
                    await send(.internal(.dislikeResponse((originalClip.isLiked, originalClip.isDisliked), .init(catching: { try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil) }))))
                }

            case .playPauseTapped:
                return .send(.delegate(.togglePlayPause(state.clip)))

            case .internal(.likeResponse(_, .success)),
                 .internal(.dislikeResponse(_, .success)):
                return .none

            case let .internal(.likeResponse(originals, .failure(error))),
                 let .internal(.dislikeResponse(originals, .failure(error))):
                state.clip.isLiked = originals.0
                state.clip.isDisliked = originals.1
                log.telemetry.error(error)
                return .none

            case .destination(.dismiss):
                state.destination = nil
                return .none

            case .setCurrentlyEditing(let isCurrentlyEditing):
                state.isCurrentlyEditing = isCurrentlyEditing
                return .none

            case .updateProgress(let progress):
                state.progress = progress
                return .none

            case .setLoadingFullClip(let isLoadingFullClip):
                state.isLoadingFullClip = isLoadingFullClip
                return .none

            case .delegate, .internal, .destination:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        Analytics()
    }
}

public struct ClipEditsListItemView: View {
    @Bindable var store: StoreOf<ClipEditsListItem>
    @State var animateWaveform = false
    @State var hapticProxy: Int = .zero
    @Environment(\.colorScheme) private var colorScheme

    let height: CGFloat = 3
    public init(store: StoreOf<ClipEditsListItem>) {
        self.store = store
    }

    public var body: some View {
        HStack(spacing: 5) {
            image
                .onTapGesture {
                    store.send(.playPauseTapped)
                }
            VStack(spacing: 0) {
                HStack(spacing: 0) {
                    label
                    Spacer()
                    controls
                        .opacity(store.isReadyToPlay ? 1 : 0.5)
                }
                .padding(.vertical, 15)
                progressBar
            }
        }
        .padding(4)
        .background {
            RoundedRectangle(cornerRadius: 4)
                .foregroundColor(store.isCurrentlyEditing ? Color.SemanticV1.backgroundQuaternary : Color.SemanticV1.backgroundSecondary)
        }
        .contentShape(.rect)
        .swipeActions {
            deleteButton(for: .deleteClip(store.clip))
        }
        .overlay(alignment: .leading) {
            if !store.hasPlayedForFirstTime {
                Circle()
                    .fill(Color.SemanticV1.iconLink)
                    .frame(width: 4, height: 4)
                    .offset(x: -6)
            }
        }
        .sheet(item: $store.scope(state: \.destination?.songActions, action: \.destination.songActions)) { store in
            SongActionsMenu(store: store)
        }
        .sensoryFeedbackIfEnabled(.impact(weight: .light), trigger: hapticProxy)
    }

    @ViewBuilder
    private var image: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 4)
                .foregroundColor(Color.SemanticV1.backgroundTertiary)
                .frame(width: 42, height: 57)
            if store.state.isReadyToPlay {
                RemoteImage(url: store.clip.imageUrl, fallbackId: store.clip.id.remoteId)
                    .clipShape(.rect(cornerRadius: 4))
                    .frame(width: 42, height: 57)
                    .clipShape(.rect(cornerRadius: 4))
            } else {
                GradientSpinner(size: .extraLarge,
                                startColor: Color.SemanticV1.auraPink,
                                endColor: Color.SemanticV1.v4Blue)
            }
        }
        .frame(width: 42, height: 57)
        .overlay(alignment: .center) {
            if store.isReadyToPlay {
                playPauseButton
            }
        }
    }

    @ViewBuilder
    private var playPauseButton: some View {
        if let playingClip = store.playingClip, store.clip.id == playingClip.id, store.isPlaying {
            Image.Icon.pause
                .foregroundColor(Color.SemanticV1.textInvert)
                .frame(width: 40, height: 40)
        } else {
            Image.Icon.playFilled
                .foregroundColor(Color.SemanticV1.textInvert)
                .frame(width: 40, height: 40)
        }
    }

    private var label: some View {
        HStack(spacing: 4) {
            Text(store.label)
                .typographyV1(.caption4.neueMontrealMedium())
                .foregroundStyle(Color.SemanticV1.textPrimary)
                .padding(.horizontal, 6)
                .padding(.vertical, 2)
                .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Color.SemanticV1.textTertiary, lineWidth: 1.0))
            if store.clip.highLevelModel == .v4 {
                Text("v4")
                    .typographyV1(.caption4)
                    .foregroundStyle(Color.SemanticV1.textPrimary)
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Color.SemanticV1.textTertiary, lineWidth: 1.0))
            }
        }
        .padding(.leading, 4)
    }

    @ViewBuilder
    private var controls: some View {
        HStack(spacing: 8) {
            Button {
                hapticProxy += 1
                store.send(.dislikeTapped)
            } label: {
                Image.Icon.thumbsUp24V1
                    .foregroundColor(store.clip.isDisliked ? Color.SemanticV1.iconPrimary : Color.SemanticV1.iconTertiary)
                    .frame(width: 24, height: 24)
                    .rotationEffect(.degrees(180))
            }
            .contentShape(.rect)
            .buttonStyle(ScaleButtonStyle(scaleAmount: 0.9))

            Button {
                hapticProxy += 1
                store.send(.likeTapped)
            } label: {
                Image.Icon.thumbsUp24V1
                    .foregroundColor(store.clip.isLiked ? Color.SemanticV1.iconPrimary : Color.SemanticV1.iconTertiary)
                    .frame(width: 24, height: 24)
            }
            .contentShape(.rect)
            .buttonStyle(ScaleButtonStyle(scaleAmount: 0.9))

            selectButton

            Button {
                hapticProxy += 1
                store.send(.moreTapped)
            } label: {
                Image.Icon.moreVertical
                    .foregroundStyle(Color.SemanticV1.iconBrand)
                    .frame(width: 24, height: 24)
            }
            .contentShape(.rect)
            .buttonStyle(ScaleButtonStyle(scaleAmount: 0.9))
        }
    }

    @ViewBuilder
    private var selectButton: some View {
        Button {
            hapticProxy += 1
            store.send(.selectTapped)
        } label: {
            ZStack {
                Text(L10n.FeatureEditClip.selectEdit)
                    .typographyV1(.caption4.neueMontrealMedium())
                    .foregroundStyle(Color.SemanticV1.backgroundPrimary)
                    .frame(width: 60, height: 24)
                    .opacity(store.isLoadingFullClip ? 0 : 1)
                    .minimumScaleFactor(0.75)
                ProgressView()
                    .progressViewStyle(.circular)
                    .tint(Color.SemanticV1.backgroundPrimary)
                    .frame(width: 10, height: 10)
                    .opacity(store.isLoadingFullClip ? 1 : 0)
            }
            .frame(width: 60, height: 24)
            .background(Color.SemanticV1.textPrimary)
            .cornerRadius(4)
        }
        .disabled(!store.isReadyToPlay)
        .contentShape(.rect)
        .buttonStyle(ScaleButtonStyle(scaleAmount: 0.9))
    }

    @ViewBuilder
    private var progressBar: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                RoundedRectangle(cornerRadius: 8)
                    .foregroundColor(Color.SemanticV1.textTertiary)
                    .frame(width: geometry.size.width)
                    .frame(height: height)
                RoundedRectangle(cornerRadius: 8)
                    .foregroundColor(Color.SemanticV1.iconLink)
                    .frame(width: abs(store.highlightedPortionEnd - store.highlightedPortionStart) / store.totalTime * geometry.size.width)
                    .frame(height: height)
                    .offset(x: store.highlightedPortionStart / store.totalTime * geometry.size.width)
                RoundedRectangle(cornerRadius: 8)
                    .foregroundColor(Color.SemanticV1.backgroundInvert)
                    .frame(width: store.offsetProgress / store.totalTime * geometry.size.width, height: height, alignment: .leading)
            }
        }
        .frame(height: height)
        .opacity(store.isReadyToPlay ? 1 : 0.5)
        .padding(.horizontal, 4)
    }

    private func deleteButton(for action: ClipEditsListItem.Action.Delegate) -> some View {
        Button {
            UIImpactFeedbackGenerator(style: .medium).impactOccurred()
            store.send(.delegate(action))
        } label: {
            Label(
                title: { Text(L10n.FeatureClipList.delete) },
                icon: {
                    Image.Icon.trashV1.renderingMode(colorScheme == .light ? .template : .original)
                }
            )
            .typographyV1(.button1)
            .foregroundStyle(Color.SemanticV1.textInvert)
            .labelStyle(.titleAndIcon)
        }
        .tint(Color.SemanticV1.backgroundInvert)
    }
}
