import APIClient
import ComponentLibrary
import ComposableArchitecture
import Errors
import EventBusClient
import FeatureEditSongArt
import FeatureToasts
import GenAPI
import Localization
import OmniPlayerClient
import StatsigClient
import SwiftUI
import Utilities

@Reducer
public struct SongDetailsReducer<Mode: SongDetailsMode> {
    public typealias State = SongDetailsState<Mode>

    @Reducer(state: .equatable)
    public enum Destination {
        case alert(AlertState<Alert>)
        case replaceSongArt(ReplaceSongArt)
        case editDisplayedLyrics(EditDisplayedLyricsReducer)
        case editDisplayStyles(EditDisplayStylesReducer)
        case moreOptions(MoreOptionsReducer)

        public enum Alert {
            case discardChanges
            case publishWithUnsavedChanges
        }
    }

    public enum Action: BindableAction {
        case task
        case destination(PresentationAction<Destination.Action>)
        case setToast(ToastType?)
        case binding(BindingAction<State>)
        case updateFields(String, String, String)
        case saveTapped
        case publishTapped
        case dismissTapped
        case clearError
        case replaceSongArtTapped
        case editLyricsTapped
        case editDisplayStylesTapped
        case moreOptionsTapped
        case previewTapped
        case remixOriginTooltipTapped
        case editStyleSummaryTooltipTapped
        case showRemixOriginToggleTapped(Bool)
        case `internal`(Internal)
        case resetFields
        case delegate(Delegate)
        case clipEvents(EventBusClient.ClipEvent)

        case onCaptionTextUpdated(String)
        case onUserMentionSearchItemTapped(SimpleProfile)
        case openUserMentionSearch

        public enum Internal {
            case dismiss
            case publish
            case saveResponse(Result<Void, Error>)
            case saveStyleSummaryResponse(Result<Void, Error>)
            case publishResponse(Result<Void, Error>)
            case toggleShowRemixOriginResponse(Result<Clip, Error>)
            case updateSuggestedUserMentions([SimpleProfile])
        }

        public enum Delegate {
            case publishSongSuccess(Clip)
        }
    }

    @Dependency(\.apiClientV2) var api
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.dismiss) var dismiss
    @Dependency(\.eventBus.sendClipEvent) private var sendClipEvent
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.commentsClient) var commentsClient
    @Dependency(OmniPlayerClient.self) var omniplayerClient

    public init() {}

    struct UserMentionSearchCancellableId: Hashable {}

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .task:
                omniplayerClient.setPlaybackConfigurationOverride(.init(repeatMode: .one, shuffle: .off))
                restoreExistingMentionsFromCaption(text: state.captionField.currentValue, state: &state)
                return .subscribe(getClipPublisher(), send: Action.clipEvents)

            case .binding:
                return .none

            case .replaceSongArtTapped:
                state.destination = .replaceSongArt(.init(clip: state.clip, showSheetNavigationBar: false))
                return .none

            case .editLyricsTapped:
                state.destination = .editDisplayedLyrics(.init(clip: state.clip))
                state.focusedField = nil
                return .none

            case .editDisplayStylesTapped:
                state.destination = .editDisplayStyles(.init(clip: state.clip))
                state.focusedField = nil
                return .none

            case .moreOptionsTapped:
                let allowComments = state.commentsAccessMap.areCommentsEnabledOnClip(state.clip.id.remoteId)
                let allowDownloads = false
                let allowRemixing = state.clip.canRemix
                let isPublishFlow = Mode.self == PublishSongMode.self // Only show 'Pin to Profile' for Publish
                state.destination = .moreOptions(.init(clip: state.clip, allowComments: allowComments, allowDownloads: allowDownloads, allowRemixing: allowRemixing, showPinToProfile: isPublishFlow))
                return .none

            case .previewTapped:
                // TODO: Add preview functionality
                return .none

            case .updateFields(let name, let caption, let displayTags):
                state.nameField.update(name)
                state.captionField.update(caption)
                state.styleSummaryField.update(displayTags)
                return .none

            case .saveTapped:
                state.isSaving = true
                let name = state.nameField.currentValue
                let caption = state.captionField.currentValue
                let styleSummary = state.styleSummaryField.currentValue

                let mentionsContainer: MentionsContainer? = {
                    let mentions = state.userMentions
                        .filter { isValidMention($0, in: caption) }
                        .map { Mention(displayName: $0.displayName, end: $0.end, handle: $0.handle, start: $0.start) }

                    return mentions.isEmpty ? nil : MentionsContainer(userMentions: mentions)
                }()

                return .run { [remoteId = state.clip.id.remoteId,
                               currentStyleSummary = state.styleSummaryField.currentValue,
                               displayTags = state.clip.displayTags] send in
                        let update = ClipMetadataSpec(
                            caption: caption,
                            captionMentions: mentionsContainer,
                            title: name
                        )
                        if currentStyleSummary != displayTags {
                            await send(.internal(.saveStyleSummaryResponse(Result(catching: { try await api.setStyleSummary(remoteId, styleSummary) }))))
                        }
                        await send(.internal(.saveResponse(Result(catching: { try await api.updateClip(remoteId, update) }))))
                }

            case .publishTapped:
                guard !state.hasUnsavedChanges else {
                    state.destination = .alert(.init(
                        title: { TextState(L10n.FeatureManageClip.publishWithUnsavedChangesAlertTitle) },
                        actions: {
                            ButtonState(action: .send(.publishWithUnsavedChanges)) { TextState(L10n.FeatureManageClip.publishWithUnsavedChangesAlertConfirm) }
                            ButtonState(role: .cancel) { TextState(L10n.FeatureManageClip.publishWithUnsavedChangesAlertCancel) }
                        },
                        message: { TextState(L10n.FeatureManageClip.publishWithUnsavedChangesAlertMessage) }
                    ))
                    return .none
                }

                return .send(.internal(.publish))

            case .internal(.publish):
                state.clip.isPublic = true
                state.isPublishing = true
                let shouldTogglePinToProfile = state.hasPendingPinToProfileChanges
                if shouldTogglePinToProfile {
                    state.clip.isPinned = !state.clip.isPinned
                    state.hasPendingPinToProfileChanges = false
                }
                return .run { [shouldTogglePinToProfile, clip = state.clip] send in
                    do {
                        try await api.setVisibility(clip, true)
                        if shouldTogglePinToProfile {
                            _ = try? await api.togglePinClip(clip.id)
                        }
                        await send(.internal(.publishResponse(.success(()))))
                    } catch {
                        await send(.internal(.publishResponse(.failure(error))))
                        return
                    }
                }

            case .dismissTapped:
                guard state.hasUnsavedChanges else {
                    return .send(.internal(.dismiss))
                }

                // User has unsaved changes, show alert
                state.destination = .alert(.init(
                    title: { TextState(L10n.FeatureManageClip.discardChangesAlertTitle) },
                    actions: {
                        ButtonState(role: .destructive, action: .send(.discardChanges)) { TextState(L10n.FeatureManageClip.discardChangesAlertConfirm) }
                        ButtonState(role: .cancel) { TextState(L10n.FeatureManageClip.discardChangesAlertCancel) }
                    },
                    message: { TextState(L10n.FeatureManageClip.discardChangesAlertMessage) }
                ))
                return .none

            case .clearError:
                state.error = nil
                return .none

            case .resetFields:
                state.nameField.reset()
                state.captionField.reset()
                state.userMentionSearchSuggestions = []
                state.userMentions = []
                return .none

            case .remixOriginTooltipTapped:
                state.showRemixOriginTooltip = true
                return .none

            case .editStyleSummaryTooltipTapped:
                state.showStyleSummaryTooltip = true
                return .none

            case .showRemixOriginToggleTapped(let show):
                state.showRemixOriginToggle = .updating(show)
                state.toggleHapticSuccess.toggle()
                return .run { [clip = state.clip] send in
                    var newClip = clip
                    newClip.showRemix = show
                    let apiResult = await Result { try await api.toggleShowRemixes(clip.id, enabled: show) }
                        .map { newClip }
                    await send(.internal(.toggleShowRemixOriginResponse(apiResult)))
                }

            case .internal(.toggleShowRemixOriginResponse(.success(let clip))):
                state.showRemixOriginToggle = .idle(clip.showRemix)
                state.clip.showRemix = clip.showRemix
                sendClipEvent(.updateClip(clip))
                return .none

            case .internal(.toggleShowRemixOriginResponse(.failure(let error))):
                /// Error alert
                state.error = error.erasedToAnyError()
                /// Doesn't do anything right now but feels right
                state.showRemixOriginToggle = .error(error.erasedToAnyError())
                log.telemetry.error(error)
                return .none

            case .internal(.saveStyleSummaryResponse(.success)):
                let displayTags = state.styleSummaryField.currentValue
                state.styleSummaryField = State.FieldState(value: displayTags)
                state.clip.displayTags = state.styleSummaryField.initialValue
                return .none

            case .internal(.saveStyleSummaryResponse(.failure(let error))):
                state.error = AnyError(error)
                return .none

            case .internal(.saveResponse(.success)):
                state.isSaving = false
                let title = state.nameField.currentValue
                let caption = state.captionField.currentValue
                state.nameField = State.FieldState(value: title)
                state.captionField = State.FieldState(value: caption)
                state.clip.title = state.nameField.initialValue
                state.clip.caption = state.captionField.initialValue

                let currentCaptionMentions = state.userMentions.filter { isValidMention($0, in: caption) }
                state.clip.captionMentions = currentCaptionMentions
                // Only clear mentions when we're actually leaving the screen (EditSongDetailsMode)
                // For PublishSongMode, keep mentions for continued editing
                if Mode.self == EditSongDetailsMode.self {
                    state.userMentions = []
                }

                state.userMentionSearchSuggestions = []
                sendClipEvent(.updateClip(state.clip))
                state.toggleHapticSuccess.toggle()

                if Mode.self == EditSongDetailsMode.self {
                    // Go back if we're in Edit Song Details
                    // Stay on the same page if we're in Post flow
                    return .send(.internal(.dismiss))
                }
                return .none

            case .internal(.saveResponse(.failure(let error))):
                state.isSaving = false
                state.error = AnyError(error)
                state.toggleHapticError.toggle()
                return .none

            case .internal(.publishResponse(.success)):
                state.isSaving = false
                sendClipEvent(.updateClip(state.clip))
                state.toggleHapticSuccess.toggle()
                return .merge(
                    .send(.delegate(.publishSongSuccess(state.clip))),
                    .send(.internal(.dismiss))
                )

            case .internal(.publishResponse(.failure(let error))):
                state.isSaving = false
                state.error = AnyError(error)
                state.toggleHapticError.toggle()
                return .none

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

            case .destination(.presented(.alert(.discardChanges))):
                state.destination = nil
                return .merge(
                    .send(.resetFields),
                    .send(.internal(.dismiss))
                )

            case .destination(.presented(.alert(.publishWithUnsavedChanges))):
                state.destination = nil
                return .merge(
                    .send(.internal(.publish)),
                    .send(.internal(.dismiss))
                )

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

            case .destination(.presented(.replaceSongArt(.delegate(.songArtUpdateSuccess(let clip))))):
                state.clip = clip
                state.destination = nil
                let successToast = ToastType.success(L10n.FeatureClipDetail.songArtUploadSuccess, .string(""))
                sendClipEvent(.updateClip(state.clip))
                return .send(.setToast(successToast))

            case .setToast(let toast):
                state.toast = toast
                return .none

            case .clipEvents(.updateClip(let updatedClip)):
                // Filters out any updates we already have, like `name` and `caption` from this screen
                // and only keeps the ones we don't, like `lyrics` from a nested screen
                guard updatedClip != state.clip else { return .none }
                state.clip = updatedClip
                return .none

            case .destination(.presented(.moreOptions(.delegate(.clipPinningUpdated)))):
                state.hasPendingPinToProfileChanges = true
                return .none

            case .onCaptionTextUpdated(let text):
                guard text != state.captionField.currentValue else { return .none }

                state.captionField.update(text)

                restoreExistingMentionsFromCaption(text: text, state: &state)
                // Clean up invalid mentions (remove from metadata, but don't modify text)
                state.userMentions = state.userMentions.filter { isValidMention($0, in: text) }

                if let userHandleToFind = extractCurrentMention(from: text) {
                    return .run { send in
                        do {
                            try await withTaskCancellation(id: UserMentionSearchCancellableId(), cancelInFlight: true) {
                                try await Task.sleep(for: .milliseconds(150)) // Debounce to avoid too many API calls
                            let matchingUsers = try await api.searchUsers(
                                nil,
                                nil,
                                userHandleToFind
                            )
                                await send(.internal(.updateSuggestedUserMentions(matchingUsers)))
                            }
                        } catch {
                            log.telemetry.error(error)
                        }
                    }
                } else {
                    state.userMentionSearchSuggestions = []
                    return .none
                }

            case .onUserMentionSearchItemTapped(let user):
                var userMentions = state.userMentions // Copy to local field to avoid overlapping access to state
                let updatedCaptionText = replacePendingMention(
                    from: state.captionField.currentValue,
                    for: user,
                    mentions: &userMentions
                )
                state.userMentions = userMentions

                state.userMentionSearchSuggestions = []
                state.captionField.update(updatedCaptionText)
                return .none

            case .openUserMentionSearch:
                let currentText = state.captionField.currentValue
                let newText: String
                if currentText.isEmpty || currentText.last == " " {
                    newText = currentText + "@"
                } else {
                    newText = currentText + " @"
                }
                return .send(.onCaptionTextUpdated(newText))

            case .internal(.updateSuggestedUserMentions(let users)):
                state.userMentionSearchSuggestions = users
                return .none

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

        Analytics()
    }
}

// Type aliases for convenience
public typealias EditSongDetailsReducer = SongDetailsReducer<EditSongDetailsMode>
public typealias PublishSongReducer = SongDetailsReducer<PublishSongMode>
