import Foundation
import Combine
import UserNotifications
import UIKit

class ContentService: ObservableObject {
    enum BannerPosition: Hashable {
        case latest
        case trending
        case conversation
        case topic(topicId: Int)
        case post(postId: Int)
        case author(authorName: String)
    }
    
    private var cancellables = Set<AnyCancellable>()
    private var authRepository: AuthRepository
    private var contentRepository: ContentRepository
    private var preferencesRepository: PreferencesRepository
    private var hydratedPostCache: [Post] = []
    
    @Published private (set) var signedInState: SignInState = .unknown
    @Published private (set) var latestPosts: [Post] = []
    @Published private (set) var trendingPosts: [Post] = []
    @Published private (set) var conversationPosts: [Post] = []
    @Published private (set) var topics: [Topic] = []
    @Published private (set) var searchFeatureTopics: [Topic] = []
    @Published private (set) var policyPages: [PolicyPage] = []
    @Published private (set) var calendar: EventCalendar = .empty
    @Published private (set) var banners: [BannerPosition:[Banner]] = [:]
    @Published private (set) var ignoreExternalWebAssets: Bool = false
    @Published private (set) var ignoreAppCSS: Bool = false
    
    init(authRepository: AuthRepository, contentRepository: ContentRepository, preferencesRepository: PreferencesRepository) {
        self.authRepository = authRepository
        self.contentRepository = contentRepository
        self.preferencesRepository = preferencesRepository
        self.setupObservers()
        
        refreshContent()
    }
    
    private func setupObservers() {
        contentRepository.latestPosts
            .receive(on: DispatchQueue.main)
            .sink { [weak self] postCollection in
                self?.latestPosts = postCollection.posts
                self?.banners[.latest] = postCollection.banners
            }.store(in: &cancellables)
        
        contentRepository.trendingPosts
            .receive(on: DispatchQueue.main)
            .sink { [weak self] postCollection in
                self?.trendingPosts = postCollection.posts
                self?.banners[.trending] = postCollection.banners
            }.store(in: &cancellables)
        
        contentRepository.conversationPosts
            .receive(on: DispatchQueue.main)
            .sink { [weak self] postCollection in
                self?.conversationPosts = postCollection.posts
                self?.banners[.conversation] = postCollection.banners
            }.store(in: &cancellables)
        
        contentRepository.events
            .receive(on: DispatchQueue.main)
            .sink { [weak self] events in
                self?.calendar = events
            }.store(in: &cancellables)
        
        contentRepository.topics
            .receive(on: DispatchQueue.main)
            .sink { [weak self] topics in
                self?.topics = topics
            }.store(in: &cancellables)
        
        contentRepository.searchFeatureTopics
            .receive(on: DispatchQueue.main)
            .sink { [weak self] topics in
                self?.searchFeatureTopics = topics
            }.store(in: &cancellables)
        
        contentRepository.policyPages
            .receive(on: DispatchQueue.main)
            .sink { [weak self] policyPages in
                self?.policyPages = policyPages
            }.store(in: &cancellables)
        
        authRepository.signedInState
            .receive(on: DispatchQueue.main)
            .sink { [weak self] signedInState in
                self?.signedInState = signedInState
            }.store(in: &cancellables)
        
        preferencesRepository.ignoreExternalWebAssetsPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                self?.ignoreExternalWebAssets = result
            }.store(in: &cancellables)
        
        preferencesRepository.ignoreAppCSSPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                self?.ignoreAppCSS = result
            }.store(in: &cancellables)
    }
    
    func refreshContent() {
        Task {
            await self.contentRepository.updateTopics()
        }
        Task {
            await self.contentRepository.updatePosts()
        }
        Task {
            let dateFrom = Date()
            let dateTo = Calendar.current.date(byAdding: .month, value: 1, to: dateFrom)
            await self.contentRepository.updateEvents(dateFrom: dateFrom, dateTo: dateTo)
        }
        Task {
            await self.contentRepository.updatePolicyPages()
        }
    }
    
    func commentsForPost(_ postId: Int) async throws -> [Comment] {
        let post = try await self.hydratePost(postId)
        
        return post.allComments ?? []
    }
    
    private func hydratedPost(_ postId: Int) -> Post? {
        if let cacheIndex = self.hydratedPostCache.firstIndex(where: { $0.id == postId }), self.hydratedPostCache[cacheIndex].isHydrated {
            return self.hydratedPostCache[cacheIndex]
        }
        
        return nil
    }
    
    func hydratePost(_ postId: Int, force: Bool = false) async throws -> Post {
        if force, let alreadyHydrated = hydratedPost(postId) {
            return alreadyHydrated
        }
        
        let post = try await self.contentRepository.hydratePost(postId)
        
        self.hydratedPostCache.append(post)
        
        // Cache banners too
        DispatchQueue.main.async { [weak self] in
            let bannerPosition = BannerPosition.post(postId: post.id)
            self?.banners[bannerPosition] = post.banners ?? []
        }
        
        return post
    }
    
    func latestPosts(page: Int = 2) async throws -> [Post] {
        let collection = try await self.contentRepository.fetchPosts(for: .latest, pageNumber: page)
        
        DispatchQueue.main.async { [weak self] in
            let bannerPosition = BannerPosition.latest
            self?.banners[bannerPosition] = collection.banners
        }
        
        return collection.posts
    }
    
    func trendingPosts(page: Int = 2) async throws -> [Post] {
        let collection = try await self.contentRepository.fetchPosts(for: .trending, pageNumber: page)
        
        DispatchQueue.main.async { [weak self] in
            let bannerPosition = BannerPosition.trending
            self?.banners[bannerPosition] = collection.banners
        }
        
        return collection.posts
    }
    
    func postsForTopic(_ topicId: Int, page: Int = 1) async throws -> [Post] {
        let collection = try await self.contentRepository.fetchPostsForTopic(topicId, page: page)
        
        DispatchQueue.main.async { [weak self] in
            let bannerPosition = BannerPosition.topic(topicId: topicId)
            self?.banners[bannerPosition] = collection.banners
        }
        
        
        return collection.posts
    }
    
    func postsForIDs(_ postIds: [Int], page: Int = 1) async throws -> [Post] {
        let collection = try await self.contentRepository.fetchPostsWithIds(postIds, page: page)
        
        return collection.posts
    }
    
    func fetchAuthorDetails(_ authorId: Int, page: Int? = nil) async throws -> AuthorResponse {
        let collection = try await self.contentRepository.fetchAuthorDetails(authorId, page: page)
        
        DispatchQueue.main.async { [weak self] in
            let bannerPosition = BannerPosition.author(authorName: collection.author.name)
            self?.banners[bannerPosition] = collection.banners
        }
        
        return collection
    }
    
    private var task: Task<[Post], Error>?
    
    func searchForPosts(_ searchTerm: String, topicIds: [Int] = []) async throws -> [Post] {
        //let collection = try await self.contentRepository.searchForPosts(searchTerm, topics: categoriesToSearch)

        let topicsToSearch = self.topics.filter({ topicIds.contains($0.id) }).compactMap { $0.topicRef }
        
        if let task = task {
            task.cancel()
        }
        
        let fetchTask = Task { () -> [Post] in
            let collection = try await self.contentRepository.searchForPosts(searchTerm, topics: topicsToSearch)
            return collection.posts
        }
        
        task = fetchTask
        
        return try await fetchTask.value
    }
    
    func bannerForPosition(_ bannerPosition: BannerPosition) -> Banner? {
        guard let banners = self.banners[bannerPosition] else {
            return nil
        }
        
        // TODO: track banners shown
        guard let banner = banners.randomElement() else {
            return nil
        }
        if banner.heading?.lowercased() == titleForBannerPosition(bannerPosition).lowercased() {
            return banner.stripHeading()
        }
        return banner
    }
    
    private func titleForBannerPosition(_ bannerPosition: BannerPosition) -> String {
        switch bannerPosition {
        case .latest:
            return "latest"
            
        case .trending:
            return "trending"
            
        case .conversation:
            return "conversation"
            
        case .topic(let topicId):
            return (self.flatTopics.first(where: {$0.id == topicId})?.name) ?? ""
            
        case .author(let authorName):
            return authorName
            
        case .post(postId: let postId):
            if let hydratedPost = hydratedPost(postId) {
                return hydratedPost.primaryTopic?.name ?? ""
            }
            return ""
        }
    }
    
    var flatTopics: [Topic] {
        topics.flatMap { topic in
            [topic] + topic.children
        }
    }
    
    func saveRecentSearchTerm(_ term: String) {
        preferencesRepository.storeRecentSearchTerm(term)
    }
    
    var recentSearchTerms: [String] {
        preferencesRepository.retrieveRecentSearchTerms()
    }
}
