import Env
import Models
import Network
import Observation
import StatusKit
import SwiftUI

@MainActor
@Observable class TimelineViewModel {
  var scrollToIndex: Int?
  var statusesState: StatusesState = .loading
  var timeline: TimelineFilter = .federated {
    willSet {
      if timeline == .home && newValue != .resume {
        saveMarker()
      }
    }
    didSet {
      timelineTask?.cancel()
      timelineTask = Task {
        await handleLatestOrResume(oldValue)
        
        if oldValue != timeline {
          await reset()
          pendingStatusesObserver.pendingStatuses = []
          tag = nil
        }
        
        guard !Task.isCancelled else {
          return
        }
        
        await fetchNewestStatuses(pullToRefresh: false)
        switch timeline {
        case let .hashtag(tag, _):
          await fetchTag(id: tag)
        default:
          break
        }
      }
    }
  }

  private(set) var timelineTask: Task<Void, Never>?

  var tag: Tag?

  // Internal source of truth for a timeline.
  private(set) var datasource = TimelineDatasource()
  private let cache = TimelineCache()
  private var isCacheEnabled: Bool {
    canFilterTimeline && timeline.supportNewestPagination
  }
  
  @ObservationIgnored
  private var visibileStatuses: [Status] = []
  
  private var canStreamEvents: Bool = true {
    didSet {
      if canStreamEvents {
        pendingStatusesObserver.isLoadingNewStatuses = false
      }
    }
  }
  
  @ObservationIgnored
  var canFilterTimeline: Bool = true

  var client: Client? {
    didSet {
      if oldValue != client {
        Task {
          await reset()
        }
      }
    }
  }

  var scrollToTopVisible: Bool = false {
    didSet {
      if scrollToTopVisible {
        pendingStatusesObserver.pendingStatuses = []
      }
    }
  }

  var serverName: String {
    client?.server ?? "Error"
  }

  var isTimelineVisible: Bool = false
  let pendingStatusesObserver: TimelineUnreadStatusesObserver = .init()
  var scrollToIndexAnimated: Bool = false
  var marker: Marker.Content?
  
  init() {
    pendingStatusesObserver.scrollToIndex = { [weak self] index in
      self?.scrollToIndexAnimated = true
      self?.scrollToIndex = index
    }
  }

  private func fetchTag(id: String) async {
    guard let client else { return }
    do {
      let tag: Tag = try await client.get(endpoint: Tags.tag(id: id))
      withAnimation {
        self.tag = tag
      }
    } catch {}
  }

  func reset() async {
    await datasource.reset()
  }
  
  private func handleLatestOrResume(_ oldValue: TimelineFilter) async {
    if timeline == .latest || timeline == .resume {
      await clearCache(filter: oldValue)
      if timeline == .resume, let marker = await fetchMarker() {
        self.marker = marker
      }
      timeline = oldValue
    }
  }

  func handleEvent(event: any StreamEvent) async {
    if let event = event as? StreamEventUpdate,
       let client,
       timeline == .home,
       canStreamEvents,
       isTimelineVisible,
       await !datasource.contains(statusId: event.status.id)
    {
      pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0)
      let newStatus = event.status
      await datasource.insert(newStatus, at: 0)
      await cache()
      StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
      let statuses = await datasource.getFiltered()
      withAnimation {
        statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
      }
    } else if let event = event as? StreamEventDelete {
      await datasource.remove(event.status)
      await cache()
      let statuses = await datasource.getFiltered()
      withAnimation {
        statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
      }
    } else if let event = event as? StreamEventStatusUpdate, let client {
      if let originalIndex = await datasource.indexOf(statusId: event.status.id) {
        StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
        await datasource.replace(event.status, at: originalIndex)
        await cache()
        let statuses = await datasource.getFiltered()
        statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
      }
    }
  }
}

// MARK: - Cache

extension TimelineViewModel {
  private func cache() async {
    if let client, isCacheEnabled {
      await cache.set(statuses: datasource.get(), client: client.id, filter: timeline.id)
    }
  }

  private func getCachedStatuses() async -> [Status]? {
    if let client, isCacheEnabled {
      return await cache.getStatuses(for: client.id, filter: timeline.id)
    }
    return nil
  }

  private func clearCache(filter: TimelineFilter) async {
    if let client, isCacheEnabled {
      await cache.clearCache(for: client.id, filter: filter.id)
      await cache.setLatestSeenStatuses([], for: client, filter: filter.id)
    }
  }
}

// MARK: - StatusesFetcher

extension TimelineViewModel: StatusesFetcher {
  func pullToRefresh() async {
    timelineTask?.cancel()
    if !timeline.supportNewestPagination || UserPreferences.shared.fastRefreshEnabled {
      await reset()
    }
    await fetchNewestStatuses(pullToRefresh: true)
  }

  func refreshTimeline() {
    timelineTask?.cancel()
    timelineTask = Task {
      if UserPreferences.shared.fastRefreshEnabled {
        await reset()
      }
      await fetchNewestStatuses(pullToRefresh: false)
    }
  }
  
  func fetchStatuses(from: Marker.Content) async throws {
    guard let client else { return }
    statusesState = .loading
    var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
                                                                              maxId: from.lastReadId,
                                                                              minId: nil,
                                                                              offset: 0))

    ReblogCache.shared.removeDuplicateReblogs(&statuses)
    StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)

    await datasource.set(statuses)
    await cache()
    statuses = await datasource.getFiltered()
    marker = nil

    withAnimation {
      statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
    }
    
    await fetchNewestStatuses(pullToRefresh: false)
  }

  func fetchNewestStatuses(pullToRefresh: Bool) async {
    guard let client else { return }
    do {
      if let marker {
        try await fetchStatuses(from: marker)
      } else if await datasource.isEmpty {
        try await fetchFirstPage(client: client)
      } else if let latest = await datasource.get().first, timeline.supportNewestPagination {
        pendingStatusesObserver.isLoadingNewStatuses = !pullToRefresh
        try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
      }
    } catch {
      statusesState = .error(error: error)
      canStreamEvents = true
    }
  }

  // Hydrate statuses in the Timeline when statuses are empty.
  private func fetchFirstPage(client: Client) async throws {
    pendingStatusesObserver.pendingStatuses = []

    if await datasource.isEmpty {
      statusesState = .loading
    }

    // If we get statuses from the cache for the home timeline, we displays those.
    // Else we fetch top most page from the API.
    if timeline.supportNewestPagination,
       let cachedStatuses = await getCachedStatuses(),
       !cachedStatuses.isEmpty,
       !UserPreferences.shared.fastRefreshEnabled
    {
      await datasource.set(cachedStatuses)
      let statuses = await datasource.getFiltered()
      if let latestSeenId = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first,
         let index = await datasource.indexOf(statusId: latestSeenId),
         index > 0
      {
        // Restore cache and scroll to latest seen status.
        statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
        scrollToIndexAnimated = false
        scrollToIndex = index + 1
      } else {
        // Restore cache and scroll to top.
        withAnimation {
          statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
        }
      }
      // And then we fetch statuses again toget newest statuses from there.
      await fetchNewestStatuses(pullToRefresh: false)
    } else {
      var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
                                                                                maxId: nil,
                                                                                minId: nil,
                                                                                offset: 0))

      ReblogCache.shared.removeDuplicateReblogs(&statuses)
      StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)

      await datasource.set(statuses)
      await cache()
      statuses = await datasource.getFiltered()

      withAnimation {
        statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
      }
    }
  }

  // Fetch pages from the top most status of the tomeline.
  private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
    canStreamEvents = false
    let initialTimeline = timeline
    var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 5)

    // Dedup statuses, a status with the same id could have been streamed in.
    let ids = await datasource.get().map(\.id)
    newStatuses = newStatuses.filter { status in
      !ids.contains(where: { $0 == status.id })
    }

    ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
    StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)

    // If no new statuses, resume streaming and exit.
    guard !newStatuses.isEmpty else {
      canStreamEvents = true
      return
    }

    // If the timeline is not visible, we don't update it as it would mess up the user position.
    guard isTimelineVisible else {
      canStreamEvents = true
      return
    }

    // Return if task has been cancelled.
    guard !Task.isCancelled else {
      canStreamEvents = true
      return
    }

    // As this is a long runnign task we need to ensure that the user didn't changed the timeline filter.
    guard initialTimeline == timeline else {
      canStreamEvents = true
      return
    }

    // Keep track of the top most status, so we can scroll back to it after view update.
    let topStatus = await datasource.getFiltered().first

    // Insert new statuses in internal datasource.
    await datasource.insert(contentOf: newStatuses, at: 0)

    // Cache statuses for timeline.
    await cache()

    // Append new statuses in the timeline indicator.
    pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)

    // High chance the user is scrolled to the top.
    // We need to update the statuses state, and then scroll to the previous top most status.
    if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id}), scrollToTopVisible {
      pendingStatusesObserver.disableUpdate = true
      let statuses = await datasource.getFiltered()
      statusesState = .display(statuses: statuses,
                               nextPageState: statuses.count < 20 ? .none : .hasNextPage)
      scrollToIndexAnimated = false
      scrollToIndex = newStatuses.count + 1
      DispatchQueue.main.async {
        self.pendingStatusesObserver.disableUpdate = false
        self.canStreamEvents = true
      }
    } else {
      // This will keep the scroll position (if the list is scrolled) and prepend statuses on the top.
      let statuses = await datasource.getFiltered()
      withAnimation {
        statusesState = .display(statuses: statuses,
                                 nextPageState: statuses.count < 20 ? .none : .hasNextPage)
        canStreamEvents = true
      }
    }

    // We trigger a new fetch so we can get the next new statuses if any.
    // If none, it'll stop there.
    // Only do that in the context of the home timeline as other don't worth catching up that much.
    if timeline == .home,
       !Task.isCancelled,
       let latest = await datasource.get().first
    {
      pendingStatusesObserver.isLoadingNewStatuses = true
      try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
    }
  }

  private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
    guard let client else { return [] }
    var pagesLoaded = 0
    var allStatuses: [Status] = []
    var latestMinId = minId
    do {
      while
        !Task.isCancelled,
        var newStatuses: [Status] =
        try await client.get(endpoint: timeline.endpoint(sinceId: nil,
                                                         maxId: nil,
                                                         minId: latestMinId,
                                                         offset: datasource.get().count)),
        !newStatuses.isEmpty,
        pagesLoaded < maxPages
      {
        pagesLoaded += 1

        ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
        StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)

        allStatuses.insert(contentsOf: newStatuses, at: 0)
        latestMinId = newStatuses.first?.id ?? ""
      }
    } catch {
      return allStatuses
    }
    return allStatuses
  }

  func fetchNextPage() async {
    guard let client else { return }
    do {
      let statuses = await datasource.get()
      guard let lastId = statuses.last?.id else { return }
      statusesState = await .display(statuses: datasource.getFiltered(), nextPageState: .loadingNextPage)
      var newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
                                                                                   maxId: lastId,
                                                                                   minId: nil,
                                                                                   offset: statuses.count))

      ReblogCache.shared.removeDuplicateReblogs(&newStatuses)

      await datasource.append(contentOf: newStatuses)
      StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)

      statusesState = await .display(statuses: datasource.getFiltered(),
                                     nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
    } catch {
      statusesState = .error(error: error)
    }
  }

  func statusDidAppear(status: Status) {
    pendingStatusesObserver.removeStatus(status: status)
    visibileStatuses.insert(status, at: 0)
    
    if let client, timeline.supportNewestPagination {
      Task {
        await cache.setLatestSeenStatuses(visibileStatuses, for: client, filter: timeline.id)
      }
    }
  }

  func statusDidDisappear(status: Status) {
    visibileStatuses.removeAll(where: { $0.id == status.id })
  }
}

// MARK: - MARKER
extension TimelineViewModel {
  func fetchMarker() async -> Marker.Content? {
    guard let client else {
      return nil
    }
    do {
      let data: Marker = try await client.get(endpoint: Markers.markers)
      return data.home
    } catch {
      return nil
    }
  }
  
  func saveMarker() {
    guard timeline == .home, let client else { return }
    Task {
      guard let id = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first else { return }
      do {
        let _: Marker = try await client.post(endpoint: Markers.markHome(lastReadId: id))
      } catch { }
    }
  }
}
