import APIClient
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureHooksCreate
import FeatureHooksModels
import FeatureHooksMoreMenu
import HookActionsClient
import FeatureToasts
import Foundation
import Localization
import NavigationRouterClient
import TabBarUtilities
import UIKit
import Utilities

@Reducer
public struct HooksGridReducer {
    public enum ViewState: Equatable {
        case initialLoading
        case empty
        case loaded(hooks: [Hook], hasMore: Bool, isLoadingMore: Bool)
        case failed(message: String)
    }

    @ObservableState
    public struct State: Equatable {
        @Shared public var me: Me
        public var hooks: [Hook] = []
        public var pendingHooks: [HooksGridConfig.PendingHook] = []
        var pages = PageState(firstPageIndex: 0)
        var loadState: LoadState = .loading
        var emptyState: HooksGridEmptyState
        var source: HooksFeedSource
        public var isCheckingForPendingHooks: Bool = false
        var hasCompletedInitialSync: Bool = false

        public var pageSize: Int = 20
        public var gridAppearance: HooksGridAppearance

        public var pendingThumbnailsDict: [String: UIImage] {
            Dictionary(uniqueKeysWithValues: pendingHooks.compactMap { pending in
                pending.thumbnail.map { (pending.hook.id, $0) }
            })
        }

        var isViewingOwnHooks: Bool {
            switch source {
            case .myHooks, .libraryGrid:
                return true
            default:
                return false
            }
        }

        public var viewState: ViewState {
            // Filter hooks for display based on whether viewing own hooks or not
            let base = isViewingOwnHooks ? hooks.userDisplayableHooks : hooks.publicDisplayableHooks
            let pendingHooksList = pendingHooks.map { $0.hook }
            let combined = pendingHooksList + base

            switch loadState {
            case .loading:
                if combined.isEmpty {
                    return .initialLoading
                } else {
                    return .loaded(hooks: combined, hasMore: pages.hasMore, isLoadingMore: pages.loadingNext)
                }

            case .loaded:
                if combined.isEmpty {
                    // Don't show empty until initial sync is complete (hooks loaded + pending hooks checked)
                    if !hasCompletedInitialSync {
                        return .initialLoading
                    }
                    return .empty
                } else {
                    return .loaded(hooks: combined, hasMore: pages.hasMore, isLoadingMore: pages.loadingNext)
                }

            case .failed(let message):
                if combined.isEmpty {
                    return .failed(message: message)
                } else {
                    return .loaded(hooks: combined, hasMore: pages.hasMore, isLoadingMore: false)
                }
            }
        }

        public init(
            dependency: HooksGridConfig
        ) {
            self._me = dependency.me
            self.gridAppearance = dependency.gridAppearance
            self.emptyState = dependency.emptyState
            self.source = dependency.source

            switch dependency.initialState {
            case .none:
                break

            case .prefetched(let hooks, let pendingHooks, let hasMore, let pageSize):
                self.hooks = hooks
                self.pendingHooks = pendingHooks
                self.loadState = .loaded
                self.pageSize = pageSize
                self.pages.update(hooks, hasAllResults: !hasMore)
                // Prefetched data is already "synced" - show it immediately
                // Pending hooks are also prefetched
                self.hasCompletedInitialSync = true

            case .loading:
                self.loadState = .loading
            }
        }
    }

    public enum Action {
        case dismiss
        case task
        case getNextPage
        case pullToRefresh
        case createHookTapped
        case hookCardTapped(Hook)
        case hookMoreTapped(Hook)
        case hookEvent(EventBusClient.HookEvent)
        case clipEvents(EventBusClient.ClipEvent)
        case hookStatusPillTapped(Hook)
        case `internal`(Internal)
        case delegate(Delegate)

        public enum Internal {
            case loadHooks
            case hooksLoadResult(page: Int, result: Result<[Hook], Error>)
            case removePendingIfMatches(videoUploadIds: [String]?)
            case startCheckingForPendingHooks
            case finishCheckingForPendingHooks
        }

        public enum Delegate {
            case hooksLoaded([Hook], isFirstPage: Bool)
        }
    }

    public init() {}

    @Dependency(\.dismiss) var dismiss
    @Dependency(\.hooksGridClient) var hooksGridClient
    @Dependency(\.toastClient.show) var showToast
    @Dependency(\.eventBus.getHookPublisher) private var getHookPublisher
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.apiClientV2) private var api
    @Dependency(NavigationRouterClient.self) private var navigationRouter
    @Dependency(\.eventBus.sendHookEvent) var sendHookEvent
    @Dependency(\.hookActionsClient) var hookActionsClient

    public var body: some ReducerOf<Self> {
        Reduce<State, Action> { state, action in
            switch action {
            case .dismiss:
                return .run { _ in await dismiss() }

            case .task:
                struct HookEventCancellableId: Hashable {}

                var effects: [Effect<Action>] = [
                    .subscribe(getHookPublisher(), send: Action.hookEvent, cancellableId: HookEventCancellableId()),
                    .subscribe(getClipPublisher(), send: Action.clipEvents),
                ]

                if state.hooks.isEmpty && state.loadState == .loading {
                    effects.append(.send(.internal(.loadHooks)))
                } else if !state.hooks.isEmpty {
                    effects.append(.send(.delegate(.hooksLoaded(state.hooks, isFirstPage: true))))
                    // Mark initial sync as complete for prefetched data (if pending check is done or not needed)
                    if !state.isCheckingForPendingHooks {
                        state.hasCompletedInitialSync = true
                    }
                } else if state.hooks.isEmpty && state.loadState == .loaded {
                    // Prefetched empty hooks - already synced, show immediately
                    // (hasCompletedInitialSync already set to true in init)
                    effects.append(.send(.delegate(.hooksLoaded([], isFirstPage: true))))
                }

                return .merge(effects)

            case .pullToRefresh:
                return .send(.internal(.loadHooks))

            case .hookEvent(let event):
                switch event {
                case .hookDeleted(hookId: let id):
                    state.hooks = state.hooks.filter { $0.id != id }
                    return .none

                case .hookUpdated(let hook):
                    guard let index = state.hooks.firstIndex(of: hook) else { return .none }
                    state.hooks[index] = hook
                    return .none

                case .hookCreated(hook: let hook):
                    // Update the hook when it's fully rendered
                    if let index = state.hooks.firstIndex(of: hook) {
                        state.hooks[index] = hook
                    } else {
                        state.hooks = [hook] + state.hooks
                    }
                    return .send(.internal(.removePendingIfMatches(videoUploadIds: hook.videoUploadIds)))

                case .hookCommentsToggled(let hookId, let canComment):
                    guard let index = state.hooks.firstIndex(where: { $0.id == hookId }) else {
                        return .none
                    }
                    state.hooks[index].allowComments = canComment
                    return .none

                default:
                    return .none
                }

            case .clipEvents(.updateClip(let updatedClip)):
                for index in state.hooks.indices {
                    if state.hooks[index].clip?.id == updatedClip.id {
                        state.hooks[index].clip = updatedClip
                    }
                }
                return .none

            case .clipEvents(.removeClip(let clip)):
                state.hooks = state.hooks.filter { $0.clip?.id != clip.id }
                return .none

            case .clipEvents:
                return .none

            case .internal(.loadHooks):
                state.pages = PageState(firstPageIndex: 0)
                state.loadState = .loading
                return .send(.getNextPage)

            case .getNextPage:
                guard state.pages.hasMore,
                      !state.pages.loadingNext,
                      let nextPage = state.pages.nextIndex()
                else {
                    return .none
                }

                return .run { send in
                    await send(.internal(.hooksLoadResult(
                        page: nextPage,
                        result: await Result(catching: {
                            try await hooksGridClient.getHooks(nextPage)
                        })
                    )))
                }

            case .internal(.hooksLoadResult(let page, .success(let newHooks))):
                state.pages.update(newHooks)
                state.loadState = .loaded

                let isFirstPage = page == 0

                if isFirstPage {
                    state.hooks = newHooks
                    // Mark initial sync as complete if pending hooks check is also done
                    if !state.isCheckingForPendingHooks {
                        state.hasCompletedInitialSync = true
                    }
                } else {
                    // Deduplicate hooks before appending to prevent duplicate IDs
                    let existingIds = Set(state.hooks.map(\.id))
                    let newUniqueHooks = newHooks.filter { !existingIds.contains($0.id) }
                    state.hooks.append(contentsOf: newUniqueHooks)
                }

                let imageUrls = newHooks.compactMap { hook in
                    hook.thumbnailImageUrl.flatMap(URL.init(string:))
                }
                if !imageUrls.isEmpty {
                    RemoteImagePrefetcher.shared.loadImages(urls: imageUrls)
                }

                return .send(.delegate(.hooksLoaded(newHooks, isFirstPage: isFirstPage)))

            case .internal(.hooksLoadResult(_, .failure(let error))):
                state.loadState = .failed(L10n.FeatureHooks.failedToLoadClips)

                // Mark initial sync as complete if pending hooks check is also done
                if !state.isCheckingForPendingHooks {
                    state.hasCompletedInitialSync = true
                }

                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 let .internal(.removePendingIfMatches(videoUploadIds)):
                guard let ids = videoUploadIds,
                      let pending = state.pendingHooks.first,
                      let pendingIds = pending.hook.videoUploadIds,
                      !ids.isEmpty,
                      !pendingIds.isEmpty
                else { return .none }

                if ids.contains(where: { pendingIds.contains($0) }) {
                    state.pendingHooks.removeAll()
                }
                return .none

            case .createHookTapped:
                sendHookEvent(.showCreateHook())
                return .none

            case .hookMoreTapped(let hook):
                sendHookEvent(.showHooksMoreMenu(hook, source: state.source))
                return .none

            case .hookCardTapped(let hook):
                // Check if the tapped hook is done processing
                guard hook.status == .renderedPassedModeration else {
                    // Hook is still processing, don't navigate
                    return .none
                }

                if let clip = hook.clip, clip.isPublic == false {
                    hookActionsClient.togglePublishClip(hook)
                    return .none
                }

                // Use filtered hooks for navigation to match what's displayed in the grid
                let hooksInGrid = state.isViewingOwnHooks ? state.hooks.userDisplayableHooks : state.hooks.publicDisplayableHooks
                guard let index = hooksInGrid.firstIndex(where: { $0.id == hook.id }) else {
                    return .none
                }

                navigationRouter.send(route: .hooksContextualFeed(hooksInGrid, index, nil, state.source))
                return .none

            case .hookStatusPillTapped(let hook):
                guard hook.status == .renderedFailedModeration,
                      let reasons = hook.contentRatingTags,
                      !reasons.isEmpty
                else {
                    return .none
                }

                sendHookEvent(.showHookStatusPopup(reasons: reasons))
                return .none

            case .internal(.startCheckingForPendingHooks):
                state.isCheckingForPendingHooks = true
                return .none

            case .internal(.finishCheckingForPendingHooks):
                state.isCheckingForPendingHooks = false
                // Mark initial sync as complete if hooks have also loaded
                if state.loadState != .loading {
                    state.hasCompletedInitialSync = true
                }
                return .none

            case .delegate:
                return .none
            }
        }
        Analytics()
    }
}
