import APIClient
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureHookSnippetSelection
import FeatureToasts
import Foundation
import HooksPlayerClient
import Localization
import NavigationRouterClient
import SnippetPlayerClient
import SwiftUI
import Utilities

@Reducer
public struct HooksSongPicker {
    @Reducer(state: .equatable)
    public enum Destination {
        case mediaPicker
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State?

        public enum Tab: CaseIterable {
            case recents
            case `public`
            case liked

            var title: String {
                switch self {
                case .recents:
                    return L10n.FeatureHooks.recents
                case .public:
                    return L10n.FeatureHooks.public
                case .liked:
                    return L10n.FeatureHooks.liked
                }
            }

            var emptyMessageText: String {
                switch self {
                case .recents:
                    return L10n.FeatureHooks.noRecentsYet
                case .public:
                    return L10n.FeatureHooks.noPublicSongsYet
                case .liked:
                    return L10n.FeatureHooks.noLikedSongsYet
                }
            }
        }

        public enum SnippetPlayerMode: Equatable {
            case playing
            case selection
        }

        var recentsClips: [ClipSnippet] = []
        var publicClips: [ClipSnippet] = []
        var likedClips: [ClipSnippet] = []

        var recentsClipsStartIndex: Int = 0
        var publicClipsStartIndex: Int = 0
        var likedClipsStartIndex: Int = 0

        var isLoadingMore: Bool = false
        var currentlyPlayingClipId: Clip.ID?
        var isPlayingCurrentClip: Bool = false
        var currentTime: Double = 0.0
        var selectedSongForOverlay: ClipSnippet?
        var snippetPlayerMode: SnippetPlayerMode = .playing

        var snippetSelectionState: HookSnippetSelectionReducer.State?

        var selectedTab: Tab = .recents

        var selectedMovie: Movie?
        var mediaTransferProgress: Double = 0
        var pendingProceedSnippet: ClipSnippet?

        var currentClips: [ClipSnippet] {
            switch selectedTab {
            case .recents:
                return recentsClips
            case .public:
                return publicClips
            case .liked:
                return likedClips
            }
        }

        public init() {}
    }

    public enum Action {
        public enum Delegate {
            case didSelectSnippet(_ snippet: ClipSnippet)
            case didSelectSnippetAndMovie(snippet: ClipSnippet, movieURL: URL)
            case dismiss
        }

        public enum SnippetPlayer {
            case playPauseTapped(ClipSnippet)
            case editTapped(ClipSnippet)
            case proceedTapped(ClipSnippet)
        }

        case fetchSongs(tab: State.Tab)
        case recentsSongsTapped
        case publicSongsTapped
        case likedSongsTapped
        case task
        case fetchSongsResponse(tab: State.Tab, result: Result<[ClipSnippet], Error>)
        case reachedEndOfSongList(tab: State.Tab)
        case snippetTapped(ClipSnippet)
        case snippetPlayer(SnippetPlayer)
        case playerEvent(SnippetPlayerClientEvent)
        case closeTapped
        case createNewSongTapped
        case delegate(Delegate)
        case snippetSelection(HookSnippetSelectionReducer.Action)
        case destination(PresentationAction<Destination.Action>)
        case presentMediaPicker
        case dismissMediaPicker
        case setSelectedMovie(Movie?)
        case setMediaTransferProgress(Double?)
    }

    @Dependency(\.apiClientV2) var apiClient
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.toastClient.show) var showToast
    @Dependency(SnippetPlayerClient.self) var playerClient
    @Dependency(\.hooksPlayerClient) var hooksPlayerClient
    @Dependency(\.eventBus.getCreateChannel) var getCreateChannel

    struct PlayerEventsSubscription: Hashable {}

    public init() {}

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .task:
                return .merge(
                    .stream(
                        playerClient.stream(),
                        send: Action.playerEvent,
                        cancellableId: PlayerEventsSubscription()
                    ),
                    .send(.fetchSongs(tab: .recents))
                )

            case .recentsSongsTapped:
                state.selectedTab = .recents
                if state.recentsClips.isEmpty {
                    return .send(.fetchSongs(tab: .recents))
                }
                return .none

            case .publicSongsTapped:
                state.selectedTab = .public
                if state.publicClips.isEmpty {
                    return .send(.fetchSongs(tab: .public))
                }
                return .none

            case .likedSongsTapped:
                state.selectedTab = .liked
                if state.likedClips.isEmpty {
                    return .send(.fetchSongs(tab: .liked))
                }
                return .none

            case .fetchSongs(let tab):
                guard !state.isLoadingMore else {
                    return .none
                }

                state.isLoadingMore = true
                let startIndex = getStartIndex(for: tab, in: state)
                let (liked, isPublic) = getTabParameters(for: tab)

                return .run { send in
                    await send(.fetchSongsResponse(
                        tab: tab,
                        result: Result(catching: {
                            try await apiClient.getHooksSuggestedClips(
                                isLiked: liked,
                                isPublicOnly: isPublic,
                                startIndex: startIndex
                            )
                        })
                    ))
                }

            case .fetchSongsResponse(let tab, .success(let clips)):
                state.isLoadingMore = false
                appendClips(clips, to: tab, in: &state)
                updateStartIndex(for: tab, with: clips.count, in: &state)
                return .none

            case .fetchSongsResponse(_, .failure(let error)):
                state.isLoadingMore = false
                if let urlError = error as? URLError, urlError.code == .cancelled {
                    return .none
                }

                let toast = ToastReducer.State.ToastType.warning(
                    L10n.FeatureHooks.failedToLoadClips,
                    position: .bottom
                )
                showToast(toast)
                return .none

            case .reachedEndOfSongList(let tab):
                guard !state.isLoadingMore && state.selectedTab == tab else {
                    return .none
                }
                return .send(.fetchSongs(tab: tab))

            case .snippetTapped(let snippet):
                if state.currentlyPlayingClipId == snippet.clip.id {
                    state.isPlayingCurrentClip.toggle()
                    if state.isPlayingCurrentClip {
                        playerClient.playCurrentClip()
                    } else {
                        playerClient.pauseCurrentClip()
                    }
                } else {
                    state.selectedSongForOverlay = snippet
                    state.currentlyPlayingClipId = snippet.clip.id
                    state.isPlayingCurrentClip = true
                    state.snippetPlayerMode = .playing
                    playerClient.loadAndPlaySnippet(snippet)
                }
                return .none

            case .snippetPlayer(let snippetPlayerAction):
                return snippetPlayerActionHandler(snippetPlayerAction, state: &state)

            case .closeTapped:
                // Let the parent decide if we need to resume the current hook
                return .send(.delegate(.dismiss))

            case .createNewSongTapped:
                navigationRouter.send(route: .library(tooltipToShow: nil, showNewClips: true))
                return .run { _ in
                    // Small delay to allow navigation to happen first
                    try await Task.sleep(for: .milliseconds(50))
                    await getCreateChannel().queue(.createClip())
                }

            case .delegate:
                return .none

            case .playerEvent(.playbackStateChanged(.playing)), .playerEvent(.playbackStateChanged(.waitingToPlayAtSpecifiedRate)):
                state.isPlayingCurrentClip = true
                return .none

            case .playerEvent(.playbackStateChanged(.paused)):
                state.isPlayingCurrentClip = false
                return .none

            case .playerEvent(.playbackTimeUpdated(currentTime: let time)):
                state.currentTime = time.seconds
                return .none

            case .playerEvent(.snippetChanged(let snippet)):
                state.currentlyPlayingClipId = snippet.clip.id
                state.isPlayingCurrentClip = true
                return .none

            case .snippetSelection(.delegate(.cancelTapped)):
                state.snippetPlayerMode = .playing
                state.snippetSelectionState = nil
                playerClient.playCurrentClip()
                return .none

            case .snippetSelection(.delegate(.saveTapped)):
                if let snippetSelectionState = state.snippetSelectionState,
                   let selectedSong = state.selectedSongForOverlay
                {
                    let newStartTime = snippetSelectionState.selectedSnippetStartTime ?? snippetSelectionState.snippetStartTime
                    let updatedSnippet = ClipSnippet(
                        clip: selectedSong.clip,
                        startTime: newStartTime,
                        endTime: selectedSong.endTime
                    )
                    state.selectedSongForOverlay = updatedSnippet
                    playerClient.loadAndPlaySnippet(updatedSnippet)
                }
                state.snippetPlayerMode = .playing
                state.snippetSelectionState = nil
                return .none

            case .snippetSelection:
                return .none

            case .presentMediaPicker:
                state.destination = .mediaPicker
                return .none

            case .dismissMediaPicker:
                state.destination = nil
                state.selectedMovie = nil
                return .none

            case .setSelectedMovie(let movie):
                state.selectedMovie = movie
                guard let movie, let snippet = state.pendingProceedSnippet else { return .none }
                state.pendingProceedSnippet = nil
                state.destination = nil
                return .send(.delegate(.didSelectSnippetAndMovie(snippet: snippet, movieURL: movie.url)))

            case .setMediaTransferProgress(let progress):
                state.mediaTransferProgress = progress ?? 0
                return .none

            case .playerEvent(.playbackStateChanged):
                return .none

            case .destination:
                return .none
            }
        }
        .ifLet(\.snippetSelectionState, action: \.snippetSelection) {
            HookSnippetSelectionReducer()
        }
        .ifLet(\.$destination, action: \.destination)
        Analytics()
    }

    // MARK: - Private Helpers

    private func getStartIndex(for tab: State.Tab, in state: State) -> Int {
        switch tab {
        case .recents:
            return state.recentsClipsStartIndex
        case .public:
            return state.publicClipsStartIndex
        case .liked:
            return state.likedClipsStartIndex
        }
    }

    private func getTabParameters(for tab: State.Tab) -> (liked: Bool, isPublic: Bool) {
        switch tab {
        case .recents:
            return (liked: false, isPublic: false)
        case .public:
            return (liked: false, isPublic: true)
        case .liked:
            return (liked: true, isPublic: false)
        }
    }

    private func appendClips(_ snippets: [ClipSnippet], to tab: State.Tab, in state: inout State) {
        switch tab {
        case .recents:
            state.recentsClips.append(contentsOf: snippets)
        case .public:
            state.publicClips.append(contentsOf: snippets)
        case .liked:
            state.likedClips.append(contentsOf: snippets)
        }
    }

    private func updateStartIndex(for tab: State.Tab, with count: Int, in state: inout State) {
        switch tab {
        case .recents:
            state.recentsClipsStartIndex += count
        case .public:
            state.publicClipsStartIndex += count
        case .liked:
            state.likedClipsStartIndex += count
        }
    }
}

private extension HooksSongPicker {
    func snippetPlayerActionHandler(
        _ action: Action.SnippetPlayer,
        state: inout State
    ) -> Effect<Action> {
        switch action {
        case .playPauseTapped(let snippet):
            if state.currentlyPlayingClipId == snippet.clip.id {
                state.isPlayingCurrentClip.toggle()
                if state.isPlayingCurrentClip {
                    playerClient.playCurrentClip()
                } else {
                    playerClient.pauseCurrentClip()
                }
            } else {
                state.currentlyPlayingClipId = snippet.clip.id
                state.isPlayingCurrentClip = true
                playerClient.loadAndPlaySnippet(snippet)
            }
            return .none

        case .editTapped(let snippet):
            guard let _ = state.selectedSongForOverlay else { return .none }
            state.snippetPlayerMode = .selection

            state.snippetSelectionState = HookSnippetSelectionReducer.State(
                duration: snippet.clip.duration,
                snippetStartTime: snippet.startTime,
                showCancelButton: true
            )
            return .none

        case .proceedTapped(let snippet):
            state.isPlayingCurrentClip = false
            playerClient.pauseCurrentClip()

            let updatedSnippet = ClipSnippet(
                clip: snippet.clip,
                startTime: state.snippetSelectionState?.selectedSnippetStartTime ?? snippet.startTime,
                endTime: snippet.endTime
            )
            state.pendingProceedSnippet = updatedSnippet
            state.destination = .mediaPicker
            return .none
        }
    }
}
