import APIClient
import AVFoundation
import Foundation
import MediaPlayer
import Utilities

/// Actor to manage HookSession
public actor HookSessionState {
    // Session tracking
    private var hookSessions: [String: HookSession] = [:]
    private var activeSessionId: String?
    private var previousSessionId: String?
    private var hookToSessionMap: [String: String] = [:]

    private var currentUserId: String?
    private var currentContextType: String?
    private var currentContextId: String?
    private var currentSourceUrl: String?
    private var currentNavigationIntent: String?
    private var currentTargetCommentId: String?
    private var currentMuteState: Bool = true

    // Track the pending play event type so recordActualPlaybackStart knows which event to send
    private var pendingPlayEventType: PlayEventType?

    // Session cleanup
    private var lastCleanupTime: Date = Date()
    private let maxSessionAge: TimeInterval = 60 * 60 // 1 hour
    private let maxSessionCount = 20
    private let cleanupInterval: TimeInterval = 15 * 60 // 15 minutes

    public struct HookSession {
        let sessionId: String
        let hookId: String
        var isOwned: Bool
        // Position when session first started (usually 0s)
        let sessionStartPosition: TimeInterval
        // Position when current play segment started (resets on play/pause)
        var currentPlaySegmentStartPosition: TimeInterval = 0
        var playEndPosition: TimeInterval = 0 // Position in hook when play ended (seconds)
        var totalPlayDuration: TimeInterval = 0 // Total time spent playing (accumulated)
        var isCurrentlyPlaying: Bool = false
        let recommendationItemId: String?
        let createdAt: Date = Date() // For cleanup purposes
        let clipId: String? = nil // When opening or closing OmniPlayer
        let previousClipId: String? = nil // When opening or closing OmniPlayer
        let previousSongSessionId: String? = nil // When opening or closing OmniPlayer

        // Time-to-first-frame tracking
        var playIntentStartTime: Date? = nil // When play() was called
        var firstFrameTime: Date? = nil // When actual playback started (.playing in timeControlStatus)
        var timeToFirstFrameSecs: TimeInterval? = nil // Calculated delay

        var playDuration: TimeInterval {
            return playEndPosition - currentPlaySegmentStartPosition
        }
    }

    public enum PlayEventType {
        case initial // First play of this hook
        case scrollDown // Scrolled down to this hook
        case scrollUp // Scrolled up to this hook
        case manual(HookPlayCause) // Manual play/resume with specific cause
        case autoRepeat // Video looped
    }

    public func getPendingPlayEventType() -> PlayEventType? {
        return pendingPlayEventType
    }

    public func clearPendingPlayEventType() {
        pendingPlayEventType = nil
    }

    public init() {
        self.currentUserId = nil
    }

    public func setCurrentUserId(_ userId: String?) {
        self.currentUserId = userId
    }

    public func setContext(contextType: String?, contextId: String?, sourceUrl: String? = nil, navigationIntent: String? = nil, targetCommentId: String? = nil) {
        self.currentContextType = contextType
        self.currentContextId = contextId
        self.currentSourceUrl = sourceUrl
        self.currentNavigationIntent = navigationIntent
        self.currentTargetCommentId = targetCommentId
    }

    public func setMuteState(_ isMuted: Bool) {
        self.currentMuteState = isMuted
    }

    // MARK: - Session Management

    public func startNewSession(for hook: Hook) {
        cleanupIfNeeded() // Clean up before creating new session

        // Store previous session ID and mark it as no longer active
        if let activeId = activeSessionId,
           let currentSession = hookSessions[activeId]
        {
            // Only store the previous session ID if it's for a different hook
            if currentSession.hookId != hook.id {
                previousSessionId = activeId
            }
            // Mark the session as no longer active
            var updatedSession = currentSession
            updatedSession.isCurrentlyPlaying = false
            hookSessions[activeId] = updatedSession
        }

        let newSessionId = UUID().uuidString
        let isOwned = currentUserId != nil && hook.user?.id == currentUserId

        hookSessions[newSessionId] = HookSession(
            sessionId: newSessionId,
            hookId: hook.id,
            isOwned: isOwned,
            sessionStartPosition: 0,
            currentPlaySegmentStartPosition: 0,
            playEndPosition: 0,
            totalPlayDuration: 0,
            isCurrentlyPlaying: false,
            recommendationItemId: hook.recommendationItemId
        )

        hookToSessionMap[hook.id] = newSessionId
        activeSessionId = newSessionId
    }

    public func recordPlayIntent(for hook: Hook, eventType: PlayEventType) {
        // Store the event type so recordActualPlaybackStart can look it up
        pendingPlayEventType = eventType

        // Check if we already have a session for this hook
        if let existingSession = getCurrentSessionForHook(hook) {
            // Reuse existing session but reset timing for new play intent
            var session = existingSession
            session.playIntentStartTime = Date()
            session.firstFrameTime = nil
            session.timeToFirstFrameSecs = nil

            if let sessionId = hookToSessionMap[hook.id] {
                hookSessions[sessionId] = session
            }
        } else {
            // Create new session only if we don't have one
            startNewSession(for: hook)

            if let sessionId = hookToSessionMap[hook.id],
               var session = hookSessions[sessionId]
            {
                session.playIntentStartTime = Date()
                session.firstFrameTime = nil
                session.timeToFirstFrameSecs = nil
                hookSessions[sessionId] = session
            }
        }
    }

    public func recordActualPlaybackStart(for hook: Hook, currentPositionSeconds: TimeInterval = 0) {
        guard let sessionId = hookToSessionMap[hook.id],
              var session = hookSessions[sessionId]
        else {
            return
        }

        let now = Date()
        session.isCurrentlyPlaying = true
        session.firstFrameTime = now

        // Calculate time-to-first-frame if we have a play intent time
        if let playIntentTime = session.playIntentStartTime {
            session.timeToFirstFrameSecs = now.timeIntervalSince(playIntentTime)
        }

        // Normalize very small start times to 0 for cleaner analytics (handles fast swiping)
        session.currentPlaySegmentStartPosition = currentPositionSeconds < 0.001 ? 0.0 : currentPositionSeconds
        hookSessions[sessionId] = session
    }

    public func startPlayback(for hook: Hook, currentPositionSeconds: TimeInterval = 0) {
        if getCurrentSessionForHook(hook) == nil {
            startNewSession(for: hook)
        }

        if let sessionId = hookToSessionMap[hook.id],
           var session = hookSessions[sessionId]
        {
            session.isCurrentlyPlaying = true
            // Normalize very small start times to 0 for cleaner analytics (handles fast swiping)
            session.currentPlaySegmentStartPosition = currentPositionSeconds < 0.001 ? 0.0 : currentPositionSeconds
            hookSessions[sessionId] = session
        }
    }

    public func pauseHookSession(_ hook: Hook, currentPositionSeconds: TimeInterval) {
        guard let sessionId = hookToSessionMap[hook.id],
              var session = hookSessions[sessionId]
        else {
            return
        }

        // Only accumulate play duration if the session was actually playing
        if session.isCurrentlyPlaying {
            session.playEndPosition = currentPositionSeconds
            let segmentDuration = session.playEndPosition - session.currentPlaySegmentStartPosition
            if segmentDuration > 0 {
                session.totalPlayDuration += segmentDuration
            }
        } else {
            // If already paused, just update the position
            session.playEndPosition = currentPositionSeconds
        }

        session.isCurrentlyPlaying = false
        hookSessions[sessionId] = session
    }

    public func endHookSession(_ hook: Hook, currentPositionSeconds: TimeInterval? = nil) {
        guard let sessionId = hookToSessionMap[hook.id],
              var session = hookSessions[sessionId]
        else {
            return
        }

        session.isCurrentlyPlaying = false

        if let currentPos = currentPositionSeconds {
            session.playEndPosition = currentPos

            let segmentDuration = session.playEndPosition - session.currentPlaySegmentStartPosition
            if segmentDuration > 0 {
                session.totalPlayDuration += segmentDuration
            }
        }

        hookSessions[sessionId] = session
    }

    public func getCurrentSessionForHook(_ hook: Hook) -> HookSession? {
        guard let sessionId = hookToSessionMap[hook.id],
              let session = hookSessions[sessionId]
        else {
            return nil
        }
        return session
    }

    // MARK: - Context Generation

    private func getCurrentVolume() -> Float? {
        // Get system volume
        let volume = AVAudioSession.sharedInstance().outputVolume

        guard volume >= 0.0, volume <= 1.0, !volume.isNaN, !volume.isInfinite else {
            return nil
        }

        return Float(round(Double(volume) * 100) / 100)
    }

    public func createContext() -> HookSessionContext {
        // Try to get ownership from the current active session
        let activeSession = getCurrentActiveSession()
        let isUserHookOwner = activeSession?.isOwned ?? false
        let recommendationItemId = activeSession?.recommendationItemId
        let timeToFirstFrameSecs = activeSession?.timeToFirstFrameSecs

        return HookSessionContext(
            hookSessionId: getCurrentSessionId() ?? UUID().uuidString,
            previousHookSessionId: previousSessionId,
            startTime: 0,
            endTime: nil,
            playDuration: nil,
            isUserHookOwner: isUserHookOwner,
            recommendationItemId: recommendationItemId,
            volume: getCurrentVolume(),
            isMuted: currentMuteState,
            contextType: currentContextType,
            contextId: currentContextId,
            sourceUrl: currentSourceUrl,
            navigationIntent: currentNavigationIntent,
            targetCommentId: currentTargetCommentId,
            timeToFirstFrameSecs: timeToFirstFrameSecs
        )
    }

    public func createContextForHook(
        _ hook: Hook,
        currentPositionSeconds _: TimeInterval = 0,
        cause: HookPlayCause? = nil // Only when needed
    ) -> HookSessionContext {
        guard let sessionId = hookToSessionMap[hook.id],
              let session = hookSessions[sessionId]
        else {
            return createContext()
        }

        var previousClipId: String? = nil
        var previousSongSessionId: String? = nil
        if let cause, case .closeOmniPlayer(let clipId, let songSessionId) = cause {
            previousClipId = clipId
            previousSongSessionId = songSessionId
        }

        return HookSessionContext(
            hookSessionId: session.sessionId,
            previousHookSessionId: previousSessionId,
            startTime: session.currentPlaySegmentStartPosition,
            endTime: nil,
            playDuration: nil,
            isUserHookOwner: session.isOwned,
            recommendationItemId: session.recommendationItemId,
            volume: getCurrentVolume(),
            isMuted: currentMuteState,
            contextType: currentContextType,
            contextId: currentContextId,
            sourceUrl: currentSourceUrl,
            navigationIntent: currentNavigationIntent,
            targetCommentId: currentTargetCommentId,
            previousClipId: previousClipId,
            previousSongSessionId: previousSongSessionId,
            timeToFirstFrameSecs: session.timeToFirstFrameSecs
        )
    }

    public func createContextForPausedHook(
        _ hook: Hook,
        currentPositionSeconds: TimeInterval,
        cause: HookPauseCause? = nil // Only passed when needed
    ) -> HookSessionContext {
        guard let sessionId = hookToSessionMap[hook.id],
              let session = hookSessions[sessionId]
        else {
            return createContext()
        }

        let startTimeSeconds = session.currentPlaySegmentStartPosition
        let endTimeSeconds = currentPositionSeconds
        let playDurationSeconds = max(0, endTimeSeconds - startTimeSeconds)
        var destinationClipId: String? = nil
        if let cause, case .openOmniPlayer(let clipId) = cause {
            destinationClipId = clipId
        }

        return HookSessionContext(
            hookSessionId: session.sessionId,
            previousHookSessionId: previousSessionId,
            startTime: startTimeSeconds,
            endTime: endTimeSeconds,
            playDuration: playDurationSeconds,
            totalPlayDuration: session.totalPlayDuration, // Already updated in pauseHookSession
            isUserHookOwner: session.isOwned,
            recommendationItemId: session.recommendationItemId,
            volume: getCurrentVolume(),
            isMuted: currentMuteState,
            contextType: currentContextType,
            contextId: currentContextId,
            sourceUrl: currentSourceUrl,
            navigationIntent: currentNavigationIntent,
            targetCommentId: currentTargetCommentId,
            clipId: destinationClipId,
            timeToFirstFrameSecs: nil // Don't include timeToFirstFrameSecs in pause events
        )
    }

    public func getCurrentSessionId() -> String? {
        return hookSessions.values.first(where: { $0.isCurrentlyPlaying })?.sessionId
    }

    public func getCurrentActiveSession() -> HookSession? {
        return hookSessions.values.first(where: { $0.isCurrentlyPlaying })
    }

    // MARK: - Session Cleanup

    private func cleanupIfNeeded() {
        let now = Date()

        // Only cleanup if enough time has passed
        if now.timeIntervalSince(lastCleanupTime) < cleanupInterval {
            return
        }

        lastCleanupTime = now
        cleanupOldSessions()
    }

    private func cleanupOldSessions() {
        let cutoffDate = Date().addingTimeInterval(-maxSessionAge)
        let sessionsToRemove = hookSessions.filter { _, session in
            session.createdAt < cutoffDate && !session.isCurrentlyPlaying
        }

        for (sessionId, session) in sessionsToRemove {
            hookSessions.removeValue(forKey: sessionId)
            hookToSessionMap.removeValue(forKey: session.hookId)
        }

        // If still too many sessions, remove oldest non-active ones
        if hookSessions.count > maxSessionCount {
            let sortedSessions = hookSessions.sorted { $0.value.createdAt < $1.value.createdAt }
            let nonActiveSessions = sortedSessions.filter { !$0.value.isCurrentlyPlaying }
            let excessCount = hookSessions.count - maxSessionCount

            for i in 0 ..< min(excessCount, nonActiveSessions.count) {
                let (sessionId, session) = nonActiveSessions[i]
                hookSessions.removeValue(forKey: sessionId)
                hookToSessionMap.removeValue(forKey: session.hookId)
            }
        }
    }
}
