import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureClipList
import FeatureManagePlaylist
import FeaturePlaylistMoreMenu
import FeatureShare
import FeatureToasts
import Glur
import Localization
import NavigationRouterClient
import Nuke
import StatsigClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

public typealias SortBy = Paths.Playlist.WithPlaylistId.GetParameters.SortBy
public typealias SortOrder = Paths.Playlist.WithPlaylistId.GetParameters.SortOrder

@Reducer
public struct PlaylistDetail {
    @Reducer(state: .equatable)
    public enum Destination {
        case renamePlaylist(CreatePlaylist)
        case share(Share)
    }

    @ObservableState
    public struct State: Equatable {
        public enum Source: Equatable {
            case playlist(Playlist)
            case playlistId(String, title: String? = nil, imageUrl: String? = nil)
            case genre(Genre)
            case section(PlaylistSection)
        }

        @Shared var me: Me

        var source: Source
        public var coverImageUrl: String?

        @ObservationStateIgnored @ObservedBox var clipList: ClipList.State
        @ObservationStateIgnored @ObservedBox var moreMenuState: PlaylistMoreMenu.State
        @ObservationStateIgnored @ObservedBox var sortingState: PlaylistSortingMenu.State

        // Applied sorting parameters for API calls
        var appliedSortBy: SortBy?
        var appliedSortOrder: SortOrder = .desc

        var loadingState: LoadState { clipList.loadState }
        public var averageColors: AverageColors = .init(average: .SemanticV1.backgroundPrimary)
        @Presents public var destination: Destination.State?

        var displayable: any PlaylistDisplayable {
            switch source {
            case .playlist(let playlist): return playlist
            case .playlistId(let id, let title, let imageUrl):
                return PlaylistPreview(id: id, title: title, imageUrl: imageUrl)
            case .genre(let genre): return genre
            case .section(let section): return section
            }
        }

        // Used when triggering the More menu, this property
        // supports both the .playlist and .playlistId source types.
        // The latter becomes available after we load the playlist
        // content.
        public var playlist: Playlist? {
            if case .playlist(let playlist) = source { return playlist }
            return nil
        }

        var avatarFallbackId: String {
            displayable.isOwned ? me.user.id : displayable.id
        }

        var amountOfClips: Int {
            if case .playlistId = source, displayable.totalResults == 0 {
                return clipList.clips.count
            }
            return displayable.totalResults
        }

        var userAvatar: String? {
            displayable.isOwned ? me.user.avatarImageUrl : displayable.authorAvatar
        }

        var userNameOrHandle: String? {
            let meNameOrHandle = me.user.displayName ?? me.user.handle
            let playlistNameOrHandle = displayable.authorName ?? displayable.authorHandle
            return displayable.isOwned ? meNameOrHandle : playlistNameOrHandle
        }

        var showMoreButton: Bool {
            displayable.canManage
        }

        var showShareButton: Bool {
            displayable.canShare
        }

        var showAuthorInfo: Bool {
            displayable.showAuthorInfo
        }

        var showSongCount: Bool {
            if case .genre = source { return false }
            return amountOfClips > 0
        }

        public init(me: Shared<Me>, source: Source) {
            self._me = me
            self.source = source

            switch source {
            case .playlist(let playlist):
                self.clipList = .init(me: me, playlist: playlist, firstPageIndex: 1, context: SessionContext(source: .playlist(playlistId: playlist.id)))
                self.moreMenuState = .init(playlistId: playlist.id, isPublic: playlist.isPublic)
                self.sortingState = .init()

            case .playlistId(let id, _, _):
                self.clipList = .init(me: me, firstPageIndex: 0, context: SessionContext(source: .playlist(playlistId: id)))
                self.moreMenuState = .init(playlistId: id, isPublic: false)
                self.sortingState = .init()

            case .genre(let genre):
                self.clipList = .init(me: me, firstPageIndex: 0, context: SessionContext(source: .style(styleId: genre.id)))
                self.moreMenuState = .init(playlistId: "", isPublic: false)
                self.sortingState = .init()
                self.clipList.pages.nextPage = .count

            case .section(let section):
                self.moreMenuState = .init(playlistId: section.id, isPublic: false)
                self.sortingState = .init()

                switch section.id {
                case PlaylistSection.Constants.continueListeningSectionId:
                    self.clipList = .init(me: me, firstPageIndex: 0, context: SessionContext(source: .listenHistory))

                case PlaylistSection.Constants.followingFeedSectionId:
                    self.clipList = .init(me: me, firstPageIndex: 0, context: SessionContext(source: .playlist(playlistId: section.id)))
                    self.clipList.pages.nextPage = .count

                default:
                    self.clipList = .init(me: me, firstPageIndex: 1, context: SessionContext(source: .playlist(playlistId: section.id)))
                }
            }
        }
    }

    public enum Action {
        public enum Internal {
            case loadAverageColors
            case averageColors(AverageColors)
            case deletePlaylistResponse(Result<Void, Error>)
            case upgradeToFullPlaylist(Playlist)
        }

        case `internal`(Internal)
        case destination(PresentationAction<Destination.Action>)
        case clipList(ClipList.Action)

        case moreMenuAction(PlaylistMoreMenu.Action)
        case sortingAction(PlaylistSortingMenu.Action)

        case onAppear
        case loadClips
        case shareTapped
        case playPlaylistTapped
        case updateClip(Clip)
        case deleteClip(Clip)
        case dismiss
        case authorTapped
    }

    @Dependency(\.dismiss) private var dismiss
    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.toastClient.show) var showToast

    private let averageColorClient = AverageColorClient()

    public init() {}

    private var clipListReducer: some ReducerOf<Self> {
        Scope(state: \.clipList, action: \.clipList) {
            ClipList()
        }
    }

    private var moreMenuReducer: some ReducerOf<Self> {
        Scope(state: \.moreMenuState, action: \.moreMenuAction) {
            PlaylistMoreMenu()
        }
    }

    private var sortingReducer: some ReducerOf<Self> {
        Scope(state: \.sortingState, action: \.sortingAction) {
            PlaylistSortingMenu()
        }
    }

    public var body: some ReducerOf<Self> {
        clipListReducer
            .withClipListClient { state in
                .init(getClips: { page in
                    switch state.source {
                    case .genre(let genre):
                        return try await api.search(page, .tagSong, genre.id, .trending).clips

                    case .section(let section):
                        switch section.id {
                        case PlaylistSection.Constants.continueListeningSectionId:
                            return try await api.getListenHistory(handle: state.me.user.handle, limit: 24).clips
                        case PlaylistSection.Constants.followingFeedSectionId:
                            return try await api.getFollowingFeed(page)
                        default:
                            return try await api.getPlaylistById(page, section.id, state.appliedSortBy, state.appliedSortOrder).clips
                        }

                    case .playlist(let playlist):
                        return try await api.getPlaylistById(page, playlist.id, state.appliedSortBy, state.appliedSortOrder).clips

                    case .playlistId(let id, _, _):
                        return try await api.getPlaylistById(page, id, state.appliedSortBy, state.appliedSortOrder).clips
                    }
                })
            }

        moreMenuReducer
        sortingReducer

        Analytics()

        Reduce<State, Action> { state, action in
            struct LoadAverageColorsID: Hashable {}

            switch action {
            case .onAppear:
                var effects: [Effect<Action>] = []
                if state.displayable.imageUrl != nil {
                    effects.append(.send(.loadClips))
                    effects.append(.send(.internal(.loadAverageColors)))
                } else {
                    effects.append(.send(.loadClips))
                }
                return .merge(effects)

            case .loadClips:
                if case .playlistId(let id, _, _) = state.source {
                    return .run { [sortBy = state.appliedSortBy, sortOrder = state.appliedSortOrder] send in
                        let response = try await api.getPlaylistById(0, id, sortBy, sortOrder)
                        await send(.internal(.upgradeToFullPlaylist(response)))
                        let clipsFeed = ClipsFeed(clips: response.clips, currentPage: 0, totalResults: response.totalResults)
                        await send(.clipList(.internal(.clipsLoadResultV2(page: 0, result: .success(clipsFeed)))))
                    }
                }
                return .send(.clipList(.loadClips))

            case .dismiss:
                return .run { _ in await self.dismiss() }

            case .shareTapped:
                guard let playlist = state.playlist else { return .none }
                state.destination = .share(.init(.playlist(playlist), me: state.$me))
                return .none

            case .playPlaylistTapped:
                return clipListReducer.reduce(into: &state, action: .clipList(.playAllClips))

            case .internal(.loadAverageColors):
                let imageUrl = state.coverImageUrl ?? state.displayable.imageUrl
                return .run { send in
                    guard let imageUrl,
                          let url = URL(string: imageUrl),
                          let image = try? await ImagePipeline.shared.image(for: url)
                    else {
                        return
                    }
                    if let color = averageColorClient.calculate(for: image) {
                        await send(.internal(.averageColors(color)))
                    }
                }
                .cancellable(id: LoadAverageColorsID())

            case .internal(.averageColors(let colors)):
                state.averageColors = colors
                return .none

            case .clipList(.delegate(.toastAfter(let toast))):
                showToast(toast)
                return .none

            case .moreMenuAction(.delegate(.toggleVisibility)):
                if case .playlist(var playlist) = state.source {
                    playlist.isPublic.toggle()
                    state.source = .playlist(playlist)
                    state.moreMenuState = .init(playlistId: playlist.id, isPublic: playlist.isPublic)
                }
                return .none

            case .moreMenuAction(.delegate(.renamePlaylist)):
                guard let playlist = state.playlist else { return .none }
                state.destination = .renamePlaylist(.init(playlist: playlist))
                return .none

            case let .destination(.presented(.renamePlaylist(.delegate(.savedPlaylist(updatedPlaylist))))):
                if case .playlist(var playlist) = state.source {
                    playlist.name = updatedPlaylist.name
                    state.source = .playlist(playlist)
                }
                return .none

            case .moreMenuAction(.delegate(.confirmDeletion)):
                guard let playlist = state.playlist else { return .none }
                return .run { send in
                    await send(.internal(.deletePlaylistResponse(Result(catching: {
                        try await apiClient.deletePlaylist(playlist.id)
                    }))))
                }

            case .sortingAction(.delegate(.sortByChanged(let sortBy))):
                state.appliedSortBy = sortBy
                return .send(.loadClips)

            case .sortingAction(.delegate(.sortOrderChanged(let sortOrder))):
                state.appliedSortOrder = sortOrder
                return .send(.loadClips)

            case .internal(.deletePlaylistResponse(.success)):
                return .send(.dismiss)

            case let .internal(.deletePlaylistResponse(.failure(error))):
                log.telemetry.error(error)
                return .none

            case .updateClip(let clip):
                return .send(.clipList(.updateClip(clip)))

            case .deleteClip(let clip):
                return .send(.clipList(.deleteClip(clip)))

            case .authorTapped:
                if let userHandle = state.displayable.authorHandle {
                    navigationRouter.send(route: .profile(userHandle))
                }
                return .none

            case .clipList(.internal(.clipsLoadResult(_, result: .success(let clips)))):
                if state.coverImageUrl == nil,
                   state.displayable.imageUrl == nil,
                   let firstClip = clips.first
                {
                    state.coverImageUrl = firstClip.largeImageUrl
                    return .send(.internal(.loadAverageColors))
                }
                return .none

            case .clipList(.internal(.clipsLoadResultV2(_, result: .success(let clipsFeed)))):
                if state.coverImageUrl == nil,
                   state.displayable.imageUrl == nil,
                   let firstClip = clipsFeed.clips.first
                {
                    state.coverImageUrl = firstClip.largeImageUrl
                    return .send(.internal(.loadAverageColors))
                }
                return .none

            case .internal(.upgradeToFullPlaylist(let playlist)):
                if case .playlistId = state.source {
                    state.source = .playlist(playlist)
                    state.moreMenuState = .init(playlistId: playlist.id, isPublic: playlist.isPublic)
                }
                return .none

            case .clipList, .destination, .moreMenuAction, .sortingAction:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
    }
}

public struct PlaylistDetailScreen: View {
    private static let top = "top"
    @State private var headerVisible = true

    @Bindable var store: StoreOf<PlaylistDetail>

    public init(store: StoreOf<PlaylistDetail>) {
        self.store = store
    }

    enum Constants {
        // The amount by which the action buttons protrude under the header image
        static let headerBottomPadding: CGFloat = 4.0
        static let parallaxStrengthFactor: CGFloat = 0.85
    }

    public var body: some View {
        ZStack {
            switch store.loadingState {
            case .loading:
                LoadingView()

            case .loaded:
                loadedView
                    .sheet(item: $store.scope(state: \.destination?.renamePlaylist, action: \.destination.renamePlaylist)) { store in
                        CreatePlaylistScreen(store: store)
                            .presentationDetents([.medium])
                    }
                    .sheet(item: $store.scope(state: \.destination?.share, action: \.destination.share)) { store in
                        ShareScreen(store: store)
                    }

            case .failed(let message):
                FailedView(
                    title: L10n.FeaturePlaylistDetail.errorTitle,
                    message: message,
                    buttonTitle: L10n.FeaturePlaylistDetail.retry,
                    action: { store.send(.loadClips) }
                )
            }
        }
        .background(Color.SemanticV1.backgroundPrimary)
        .navigationBarBackButtonHidden()
        .animation(.default, value: store.loadingState)
        .onAppear { store.send(.onAppear) }
    }

    private var loadedView: some View {
        StretchingHeaderContent(
            header: { offset in
                RemoteImage(url: store.coverImageUrl ?? store.displayable.imageUrl, fallbackId: store.displayable.id)
                    .padding(.top, offset > 0 ? offset * Constants.parallaxStrengthFactor : 0)
                    .clipped()
                    .glur(radius: 16.0, offset: 0.7, interpolation: 0.2, drawingGroup: false)
                    .padding(.bottom, Constants.headerBottomPadding)
                    .overlay(alignment: .bottom) {
                        playlistMetadata
                    }
                    .onAppear { headerVisible = true }
                    .onDisappear { headerVisible = false }
                    .id(Self.top)
                    .padding(.bottom, 20.0)
            },
            content: {
                if store.clipList.clips.isEmpty {
                    emptyView
                } else {
                    ClipListContent(store: store.scope(state: \.clipList, action: \.clipList), isReorderable: store.displayable.isOwned) { _, item in
                        item
                            .padding(.horizontal, 12)
                    }
                }
            }
        )
        .disablePressDelay()
        .toolbarBackground(headerVisible ? .hidden : .visible, for: .navigationBar)
        .toolbar {
            ToolbarItem(placement: .principal) {
                ToolbarTitle(store.displayable.title, visibility: !headerVisible)
            }

            ToolbarItemGroup(placement: .navigationBarTrailing) {
                if #available(iOS 26.0, *) {
                    trailingToolbar
                } else {
                    HStack(spacing: 0) {
                        trailingToolbar
                    }
                }
            }
        }
    }

    @ViewBuilder
    private var trailingToolbar: some View {
        PlaylistSortingMenuView(
            store: store.scope(state: \.sortingState, action: \.sortingAction),
            showHeader: headerVisible,
            colorScheme: store.averageColors.colorScheme
        )

        if store.showMoreButton {
            PlaylistMoreMenuView(
                store: store.scope(state: \.moreMenuState, action: \.moreMenuAction),
                showHeader: headerVisible,
                colorScheme: store.averageColors.colorScheme
            )
        }
    }

    private var playlistMetadata: some View {
        VStack {
            Group {
                Text(store.displayable.title)
                    .typographyV1(.headline2)
                    .lineLimit(1)
                    .minimumScaleFactor(0.5)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .animation(.default, value: store.displayable.title)
                    .blendMode(.luminosity)

                HStack(spacing: 4) {
                    if store.showAuthorInfo, let usernameOrHandle = (store.displayable.authorName ?? store.displayable.authorHandle), !usernameOrHandle.isEmpty {
                        RemoteImage(url: store.displayable.authorAvatar, fallbackId: store.avatarFallbackId)
                            .frame(width: 16, height: 16)
                            .clipShape(Circle())

                        authorLabel(usernameOrHandle)

                        Spacer()

                        if !store.displayable.isPublic {
                            privateLabel
                        }
                    } else if store.showSongCount {
                        Text(L10n.FeaturePlaylistDetail.songs(store.amountOfClips))
                            .typographyV1(.caption2)
                            .lineLimit(1)
                            .blendMode(.luminosity)

                        Spacer()
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .foregroundStyle(Color.SemanticV1.textPrimary)
            .environment(\.colorScheme, store.averageColors.colorScheme)
            .padding(.horizontal, 12)

            actionButtons
                .padding(.top, 16)
                .padding(.horizontal, 12)
        }
        .background(alignment: .bottom) {
            bottomGradient
        }
    }

    private func authorLabel(_ usernameOrHandle: String) -> some View {
        Button(action: {
            store.send(.authorTapped)
        }) {
            HStack(spacing: 4) {
                Text(usernameOrHandle)
                if store.showSongCount {
                    Text(verbatim: "•")
                    Text(L10n.FeaturePlaylistDetail.songs(store.amountOfClips))
                }
            }
            .typographyV1(.caption2)
            .lineLimit(1)
        }
        .blendMode(.luminosity)
    }

    private var privateLabel: some View {
        HStack {
            Image.Icon.lockV1
                .resizable()
                .scaledToFit()
                .frame(height: 18)

            Text(L10n.FeaturePlaylistDetail.private)
                .typographyV1(.caption2)
        }
        .blendMode(.luminosity)
    }

    private var bottomGradient: some View {
        LinearGradient.eased(
            startColor: store.averageColors.average.opacity(0),
            endColor: Color.SemanticV1.backgroundPrimary,
            stops: 16,
            easing: { t in
                t * t * (3 - 2 * t)
            }
        )
        .padding(.bottom, Constants.headerBottomPadding)
        .padding(.top, 12)
    }

    private var emptyView: some View {
        VStack(spacing: 8) {
            Text(L10n.FeaturePlaylistDetail.emptyTitle)
                .typographyV1(.headline4)

            Text(L10n.FeaturePlaylistDetail.emptyMessage)
                .typographyV1(.body1Wide)
                .opacity(0.5)
        }
        .foregroundColor(Color.SemanticV1.textPrimary)
        .frame(maxWidth: .infinity)
        .frame(height: 100)
        .listRowSeparator(.hidden)
        .listRowBackground(Color.clear)
    }

    private var actionButtons: some View {
        HStack(spacing: 12) {
            PrimaryButtonV1(
                title: L10n.FeatureClipDetail.play,
                colorCombination: .playlist,
                preferredSize: .playlistMedium,
                leadingView: { Image.Icon.playFilled },
                action: { store.send(.playPlaylistTapped) }
            )

            if store.showShareButton {
                PrimaryButtonV1(
                    isIconOnly: true,
                    colorCombination: .playlist,
                    preferredSize: .playlistMediumAdaptive,
                    leadingView: { Image.Icon.share },
                    action: { store.send(.shareTapped) }
                )
                .aspectRatio(1.0, contentMode: .fit)
            }
        }
        .listRowSeparator(.hidden)
    }
}

private extension PillButtonStyleV1.ColorCombination {
    static var playlist: PillButtonStyleV1.ColorCombination {
        PillButtonStyleV1.ColorCombination(
            enabled: PillButtonStyleV1.ColorSet(
                foreground: .SemanticV1.textPrimary,
                background: .SemanticV1.backgroundSecondary.opacity(0.5),
                loading: .SemanticV1.iconPrimary,
                border: .clear
            ),
            pressed: PillButtonStyleV1.ColorSet(
                foreground: .SemanticV1.textPrimary.opacity(0.5),
                background: .SemanticV1.backgroundTertiary.opacity(0.5),
                loading: .SemanticV1.iconPrimary.opacity(0.5),
                border: .clear
            ),
            disabled: PillButtonStyleV1.ColorSet(
                foreground: .SemanticV1.textPrimary.opacity(0.5),
                background: .SemanticV1.backgroundPrimary.opacity(0.25),
                loading: .SemanticV1.iconTertiary.opacity(0.5),
                border: .clear
            ),
            glassType: .regular
        )
    }
}

private extension PillButtonSizeV1 {
    static let playlistMedium = PillButtonSizeV1(
        typography: .button1,
        width: nil,
        maxWidth: .infinity,
        minHeight: 48,
        iconWidth: 24,
        borderRadius: 24,
        padding: EdgeInsets()
    )

    static let playlistMediumAdaptive = PillButtonSizeV1(
        typography: .button1,
        width: nil,
        maxWidth: nil,
        minHeight: 48,
        iconWidth: 24,
        borderRadius: 24,
        padding: EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)
    )
}
