import ActivityKit
import Foundation
import SwiftUI
import UIKit

@MainActor
final class LiveActivityManager: ObservableObject {
    static let shared = LiveActivityManager()

    @Published private(set) var isFollowing = false
    @Published private(set) var lastError: String?
    @Published var shouldShowPrompt = false

    private let userDefaults: UserDefaults
    private let promptAskedKey = "liveActivityPromptAsked"
    private let preferenceEnabledKey = "liveActivityPreferenceEnabled"
    private var currentActivityID: String?
    private var tokenObservationTask: Task<Void, Never>?
    private var lastLocalUpdateAt: Date?
    private var lastContentState: CharacterLiveActivityAttributes.ContentState?

    private init(userDefaults: UserDefaults = .standard) {
        self.userDefaults = userDefaults
        refreshFollowingState()
    }

    var isAvailable: Bool {
        ActivityAuthorizationInfo().areActivitiesEnabled
    }

    var statusLabel: String {
        if !isAvailable {
            return "Unavailable"
        }
        return isFollowing ? "On" : "Off"
    }

    var statusHint: String {
        if !isAvailable {
            return "Turn on Live Activities in iOS Settings."
        }
        if isFollowing {
            return "Following"
        }
        return "Off"
    }

    func handlePlayableSession(webSocketService: WebSocketService) async {
        refreshFollowingState(characterId: webSocketService.person.id)

        guard webSocketService.isConnected else {
            return
        }

        let person = webSocketService.person
        let player = webSocketService.player

        guard person.status != "dead", player.status != "creating" else {
            await endCurrentActivity(webSocketService: webSocketService, disablePreference: false)
            return
        }

        let isPlayable = LiveActivityPromptPolicy.isPlayable(person: person, player: player)

        if LiveActivityPromptPolicy.shouldShowPrompt(
            promptAsked: hasAskedPrompt,
            isAvailable: isAvailable,
            isPlayable: isPlayable
        ) {
            shouldShowPrompt = true
            return
        }

        guard userDefaults.bool(forKey: preferenceEnabledKey), isPlayable else {
            return
        }

        if !isAvailable {
            lastError = "Turn on Live Activities in iOS Settings."
            return
        }

        if currentActivity(for: person.id) == nil {
            await startFollowing(webSocketService: webSocketService)
        } else {
            await update(webSocketService: webSocketService)
        }
    }

    func startFollowing(webSocketService: WebSocketService) async {
        userDefaults.set(true, forKey: promptAskedKey)
        userDefaults.set(true, forKey: preferenceEnabledKey)
        shouldShowPrompt = false

        guard isAvailable else {
            lastError = "Turn on Live Activities in iOS Settings."
            refreshFollowingState(characterId: webSocketService.person.id)
            return
        }

        guard LiveActivityPromptPolicy.isPlayable(
            person: webSocketService.person,
            player: webSocketService.player
        ) else {
            refreshFollowingState(characterId: webSocketService.person.id)
            return
        }

        let person = webSocketService.person
        let player = webSocketService.player

        if let activity = currentActivity(for: person.id) {
            currentActivityID = activity.id
            isFollowing = true
            observePushTokenUpdates(for: activity, webSocketService: webSocketService)
            await update(webSocketService: webSocketService, force: true)
            return
        }

        let avatarCacheKey = LiveActivitySnapshotBuilder.avatarCacheKey(for: person)
        await cacheAvatarIfPossible(from: person.image, cacheKey: avatarCacheKey)

        let attributes = CharacterLiveActivityAttributes(
            characterId: person.id,
            characterName: LiveActivitySnapshotBuilder.characterName(for: person),
            avatarCacheKey: avatarCacheKey,
            startedAt: Date()
        )

        let state = LiveActivitySnapshotBuilder.makeContentState(
            person: person,
            player: player,
            isRunning: player.status == "playing",
            now: Date()
        )

        do {
            let activity = try Activity<CharacterLiveActivityAttributes>.request(
                attributes: attributes,
                content: ActivityContent(
                    state: state,
                    staleDate: Date().addingTimeInterval(5 * 60)
                ),
                pushType: .token
            )

            currentActivityID = activity.id
            isFollowing = true
            lastError = nil
            lastContentState = state
            lastLocalUpdateAt = Date()
            observePushTokenUpdates(for: activity, webSocketService: webSocketService)
        } catch {
            lastError = "Live Activity could not start."
            userDefaults.set(false, forKey: preferenceEnabledKey)
            refreshFollowingState(characterId: person.id)
        }
    }

    func declinePrompt() {
        userDefaults.set(true, forKey: promptAskedKey)
        userDefaults.set(false, forKey: preferenceEnabledKey)
        shouldShowPrompt = false
    }

    func stopFollowing(webSocketService: WebSocketService) async {
        await endCurrentActivity(webSocketService: webSocketService, disablePreference: true)
    }

    func update(
        webSocketService: WebSocketService,
        force: Bool = false
    ) async {
        guard let activity = currentActivity(for: webSocketService.person.id) else {
            refreshFollowingState(characterId: webSocketService.person.id)
            return
        }

        let state = LiveActivitySnapshotBuilder.makeContentState(
            person: webSocketService.person,
            player: webSocketService.player,
            isRunning: webSocketService.player.status == "playing",
            now: Date()
        )

        let majorChange = LiveActivitySnapshotBuilder.isMajorVisibleChange(
            from: lastContentState,
            to: state
        )

        guard force || LiveActivityUpdateThrottle.shouldUpdate(
            lastUpdate: lastLocalUpdateAt,
            now: Date(),
            majorChange: majorChange
        ) else {
            return
        }

        await activity.update(
            ActivityContent(
                state: state,
                staleDate: Date().addingTimeInterval(5 * 60)
            )
        )

        currentActivityID = activity.id
        isFollowing = true
        lastError = nil
        lastContentState = state
        lastLocalUpdateAt = Date()
    }

    private var hasAskedPrompt: Bool {
        userDefaults.bool(forKey: promptAskedKey)
    }

    private func endCurrentActivity(
        webSocketService: WebSocketService,
        disablePreference: Bool
    ) async {
        if disablePreference {
            userDefaults.set(false, forKey: preferenceEnabledKey)
        }

        shouldShowPrompt = false
        tokenObservationTask?.cancel()
        tokenObservationTask = nil

        let activities = matchingActivities(characterId: webSocketService.person.id)
        let finalState = LiveActivitySnapshotBuilder.makeContentState(
            person: webSocketService.person,
            player: webSocketService.player,
            isRunning: false,
            now: Date()
        )

        for activity in activities {
            sendLiveActivityEnded(activityId: activity.id, webSocketService: webSocketService)
            await activity.end(
                ActivityContent(state: finalState, staleDate: nil),
                dismissalPolicy: .immediate
            )
        }

        currentActivityID = nil
        isFollowing = false
        lastContentState = nil
        lastLocalUpdateAt = nil
    }

    private func refreshFollowingState(characterId: String? = nil) {
        if let characterId, let activity = currentActivity(for: characterId) {
            currentActivityID = activity.id
            isFollowing = true
            return
        }

        let activities = Activity<CharacterLiveActivityAttributes>.activities
        currentActivityID = activities.first?.id
        isFollowing = !activities.isEmpty
    }

    private func currentActivity(
        for characterId: String
    ) -> Activity<CharacterLiveActivityAttributes>? {
        let activities = Activity<CharacterLiveActivityAttributes>.activities

        if let currentActivityID,
           let current = activities.first(where: { $0.id == currentActivityID }) {
            return current
        }

        return activities.first { $0.attributes.characterId == characterId }
    }

    private func matchingActivities(
        characterId: String
    ) -> [Activity<CharacterLiveActivityAttributes>] {
        let activities = Activity<CharacterLiveActivityAttributes>.activities
        if let currentActivityID {
            return activities.filter { $0.id == currentActivityID || $0.attributes.characterId == characterId }
        }
        return activities.filter { $0.attributes.characterId == characterId }
    }

    private func observePushTokenUpdates(
        for activity: Activity<CharacterLiveActivityAttributes>,
        webSocketService: WebSocketService
    ) {
        tokenObservationTask?.cancel()
        tokenObservationTask = Task { [weak self, weak webSocketService] in
            for await tokenData in activity.pushTokenUpdates {
                await MainActor.run {
                    guard let self, let webSocketService else { return }
                    self.sendLiveActivityToken(
                        tokenData,
                        activity: activity,
                        webSocketService: webSocketService
                    )
                }
            }
        }
    }

    private func sendLiveActivityToken(
        _ tokenData: Data,
        activity: Activity<CharacterLiveActivityAttributes>,
        webSocketService: WebSocketService
    ) {
        webSocketService.sendMessage(message: [
            "type": "liveActivityToken",
            "message": [
                "activityId": activity.id,
                "token": tokenData.hexadecimalString,
                "characterId": activity.attributes.characterId,
            ],
        ])
    }

    private func sendLiveActivityEnded(
        activityId: String,
        webSocketService: WebSocketService
    ) {
        webSocketService.sendMessage(message: [
            "type": "liveActivityEnded",
            "message": [
                "activityId": activityId,
            ],
        ])
    }

    private func cacheAvatarIfPossible(from imageURLString: String, cacheKey: String) async {
        guard let targetURL = LiveActivitySharedConfiguration.avatarURL(for: cacheKey),
              let imageURL = URL(string: imageURLString),
              !imageURLString.isEmpty else {
            return
        }

        do {
            let (data, _) = try await URLSession.shared.data(from: imageURL)
            guard let image = UIImage(data: data),
                  let resizedData = resizedPNGData(from: image) else {
                return
            }

            try resizedData.write(to: targetURL, options: [.atomic])
        } catch {
            #if DEBUG
            print("Live Activity avatar cache failed: \(error)")
            #endif
        }
    }

    private func resizedPNGData(from image: UIImage) -> Data? {
        let size = CGSize(
            width: LiveActivitySharedConfiguration.avatarPixelSize,
            height: LiveActivitySharedConfiguration.avatarPixelSize
        )

        let renderer = UIGraphicsImageRenderer(size: size)
        let rendered = renderer.image { _ in
            let aspectFillScale = max(
                size.width / image.size.width,
                size.height / image.size.height
            )
            let scaledSize = CGSize(
                width: image.size.width * aspectFillScale,
                height: image.size.height * aspectFillScale
            )
            let origin = CGPoint(
                x: (size.width - scaledSize.width) / 2,
                y: (size.height - scaledSize.height) / 2
            )
            image.draw(in: CGRect(origin: origin, size: scaledSize))
        }

        return rendered.pngData()
    }
}

private extension Data {
    var hexadecimalString: String {
        map { String(format: "%02x", $0) }.joined()
    }
}
