package solutions.tretter.mindmachine.session

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import solutions.tretter.mindmachine.audio.BinauralAudioEngine
import solutions.tretter.mindmachine.audio.HeadsetMonitor
import solutions.tretter.mindmachine.data.SettingsRepository
import solutions.tretter.mindmachine.data.UserProgramEntity
import solutions.tretter.mindmachine.data.UserProgramRepository
import solutions.tretter.mindmachine.domain.AppSettings
import solutions.tretter.mindmachine.domain.Presets
import solutions.tretter.mindmachine.domain.RuntimeState
import solutions.tretter.mindmachine.domain.SessionConfig
import solutions.tretter.mindmachine.domain.SessionMode
import solutions.tretter.mindmachine.domain.SessionPreset
import solutions.tretter.mindmachine.domain.toConfig
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID

const val SAFETY_VERSION = 1

data class UiState(
    val settingsInitialized: Boolean = false,
    val settings: AppSettings = AppSettings(),
    val presets: List<SessionPreset> = Presets.builtIn,
    val selectedPreset: SessionPreset = Presets.builtIn.first(),
    val config: SessionConfig = Presets.builtIn.first().toConfig(),

    // Only user-saved programs (including customizations of built-ins) are deletable.
    val deletableProgramIds: Set<String> = emptySet(),

    // Timeline program (curves) used for both editing + runtime modulation.
    val timeline: TimelineEditorState = TimelineProgramFactory.fromPreset(Presets.builtIn.first()),
    val savedProgramName: String = Presets.builtIn.first().name,
    val savedTimelineSignature: TimelineProgramSignature = timelineProgramSignature(TimelineProgramFactory.fromPreset(Presets.builtIn.first())),
    val hasUnsavedChanges: Boolean = false,

    val runtimeState: RuntimeState = RuntimeState.IDLE,
    val error: String? = null,
    val remainingSec: Int = 0,
    val countdownSec: Int = 0,
    val endedEarly: Boolean = false,
    val interruptionReason: String? = null,

    // Current runtime-evaluated parameters (updated while running).
    val runtimeParams: RuntimeParams = RuntimeParams(),
)

data class TimelineProgramSignature(
    val durationSec: Int,
    val curveGranularitySec: Int,
    val flashSequence: List<FlashSequenceRow>,
    val visualLeft: List<TimelinePoint>,
    val visualRight: List<TimelinePoint>,
    val audioLeft: List<TimelinePoint>,
    val audioRight: List<TimelinePoint>,
)

private fun timelineProgramSignature(timeline: TimelineEditorState): TimelineProgramSignature = TimelineProgramSignature(
    durationSec = timeline.durationSec,
    curveGranularitySec = timeline.curveGranularitySec,
    flashSequence = timeline.flashSequence,
    visualLeft = timeline.visualLeft.points,
    visualRight = timeline.visualRight.points,
    audioLeft = timeline.audioLeft.points,
    audioRight = timeline.audioRight.points,
)

private fun isDirty(name: String, timeline: TimelineEditorState, savedName: String, savedTimelineSignature: TimelineProgramSignature): Boolean {
    val timelineChanged = timelineProgramSignature(timeline) != savedTimelineSignature
    val nameChanged = name.trim() != savedName.trim()
    return timelineChanged || nameChanged
}

class MainViewModel(
    private val settingsRepository: SettingsRepository,
    private val userProgramRepository: UserProgramRepository,
    private val headsetMonitor: HeadsetMonitor,
    private val audioEngine: BinauralAudioEngine,
    private val billingManager: solutions.tretter.mindmachine.billing.BillingManager,
) : ViewModel() {
    private val _ui = MutableStateFlow(UiState())
    val ui: StateFlow<UiState> = _ui.asStateFlow()

    private var runJob: Job? = null
    private var elapsedBeforePauseSec: Float = 0f
    private var cachedPrograms: List<UserProgramEntity> = emptyList()

    init {
        billingManager.start()

        // One-time first-run defaults.
        viewModelScope.launch {
            settingsRepository.ensureFirstRunDefaults()
        }

        viewModelScope.launch {
            combine(settingsRepository.settings, userProgramRepository.userPrograms) { settings, userPrograms ->
                settings to userPrograms
            }.collect { (settings, userPrograms) ->
                val userProgramIds = userPrograms.map { it.id }.toSet()
                val builtInPrograms = Presets.builtIn.map { preset ->
                    userPrograms.firstOrNull { it.id == preset.id } ?: UserProgramEntity(
                        id = preset.id,
                        name = preset.name,
                        description = preset.description,
                        timeline = TimelineProgramFactory.fromPreset(preset),
                        sortOrder = preset.sortOrder,
                    )
                }
                val mergedPrograms = (builtInPrograms + userPrograms.filterNot { program -> Presets.builtIn.any { it.id == program.id } })
                    .sortedBy { it.sortOrder }
                cachedPrograms = mergedPrograms

                val allPresets = mergedPrograms.map {
                    TimelineProgramFactory.toPreset(
                        id = it.id,
                        name = it.name,
                        description = it.description,
                        timeline = it.timeline,
                        sortOrder = it.sortOrder,
                        // Deletable only when the user explicitly saved it (either custom program,
                        // or a customized built-in stored in the user program store).
                        isUserProgram = it.id in userProgramIds,
                    )
                }

                _ui.update { current ->
                    val selectedId = settings.lastPresetId ?: current.selectedPreset.id
                    val selectedPreset = allPresets.find { it.id == selectedId } ?: allPresets.first()
                    val sameSelection = selectedPreset.id == current.selectedPreset.id
                    val nextTimeline = if (sameSelection) current.timeline else timelineForProgramId(selectedPreset.id, mergedPrograms)
                    val savedName = if (sameSelection) current.savedProgramName else selectedPreset.name
                    val savedSignature = if (sameSelection) current.savedTimelineSignature else timelineProgramSignature(nextTimeline)
                    current.copy(
                        settingsInitialized = true,
                        settings = settings,
                        presets = allPresets,
                        selectedPreset = selectedPreset,
                        config = current.config.copy(mode = SessionMode.AUDIO_VISUAL),
                        timeline = nextTimeline,
                        savedProgramName = savedName,
                        savedTimelineSignature = savedSignature,
                        hasUnsavedChanges = isDirty(selectedPreset.name, nextTimeline, savedName, savedSignature),
                        deletableProgramIds = userProgramIds,
                    )
                }
            }
        }
    }

    fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) }

    fun choosePreset(id: String) {
        applyPresetSelection(id)
    }

    fun startSessionFromProgramSelection(id: String): Boolean {
        applyPresetSelection(id)
        return startSession()
    }

    fun createNewProgramDraft() {
        val draftId = "user-${UUID.randomUUID()}"
        val displayName = nextUntitledName()
        val timeline = TimelineEditorState.default(20 * 60)
        val preset = TimelineProgramFactory.toPreset(
            id = draftId,
            name = displayName,
            description = "Custom program",
            timeline = timeline,
            sortOrder = nextSortOrder(),
            isUserProgram = true,
        )
        _ui.update {
            val savedSignature = timelineProgramSignature(timeline)
            it.copy(
                selectedPreset = preset,
                config = preset.toConfig(SessionMode.AUDIO_VISUAL).copy(flashSequence = timeline.flashSequence),
                timeline = timeline,
                savedProgramName = displayName,
                savedTimelineSignature = savedSignature,
                hasUnsavedChanges = false,
                error = null,
            )
        }
    }

    fun saveCurrentProgram(name: String) = viewModelScope.launch {
        val trimmed = name.trim()
        if (trimmed.isBlank()) {
            _ui.update { it.copy(error = "Enter a name before saving.") }
            return@launch
        }

        val state = _ui.value
        val id = state.selectedPreset.id
        val sortOrder = cachedPrograms.find { it.id == id }?.sortOrder ?: nextSortOrder()

        userProgramRepository.upsert(
            UserProgramEntity(
                id = id,
                name = trimmed,
                description = "Custom program",
                timeline = state.timeline,
                sortOrder = sortOrder,
            )
        )
        settingsRepository.updateLastPreset(id)

        _ui.update {
            val savedPreset = TimelineProgramFactory.toPreset(
                id = id,
                name = trimmed,
                description = "Custom program",
                timeline = it.timeline,
                sortOrder = sortOrder,
                isUserProgram = true,
            )
            val savedSignature = timelineProgramSignature(it.timeline)
            it.copy(
                selectedPreset = savedPreset,
                savedProgramName = trimmed,
                savedTimelineSignature = savedSignature,
                hasUnsavedChanges = false,
                error = "Saved \"$trimmed\"",
            )
        }
    }

    fun deleteProgram(id: String) = viewModelScope.launch {
        userProgramRepository.delete(id)
        if (_ui.value.selectedPreset.id == id) {
            val fallback = _ui.value.presets.firstOrNull { it.id != id } ?: Presets.builtIn.first()
            settingsRepository.updateLastPreset(fallback.id)
            _ui.update {
                val timeline = timelineForProgramId(fallback.id, cachedPrograms)
                val savedSignature = timelineProgramSignature(timeline)
                it.copy(
                    selectedPreset = fallback,
                    config = fallback.toConfig(SessionMode.AUDIO_VISUAL).copy(flashSequence = timeline.flashSequence),
                    timeline = timeline,
                    savedProgramName = fallback.name,
                    savedTimelineSignature = savedSignature,
                    hasUnsavedChanges = false,
                    error = null,
                )
            }
        }
    }

    fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }

    fun setDurationSec(seconds: Int) = _ui.update {
        val clamped = seconds.coerceIn(60, 60 * 60)
        val nextTimeline = it.timeline.setDurationSec(clamped)
        it.copy(
            config = it.config.copy(durationSec = clamped),
            timeline = nextTimeline,
            hasUnsavedChanges = isDirty(it.selectedPreset.name, nextTimeline, it.savedProgramName, it.savedTimelineSignature),
            error = null,
        )
    }

    fun setCurveGranularitySec(seconds: Int) = _ui.update {
        val nextTimeline = it.timeline.setCurveGranularitySec(seconds)
        it.copy(
            timeline = nextTimeline,
            hasUnsavedChanges = isDirty(it.selectedPreset.name, nextTimeline, it.savedProgramName, it.savedTimelineSignature),
            error = null,
        )
    }

    fun updateTimeline(newState: TimelineEditorState) = _ui.update {
        it.copy(
            timeline = newState,
            config = it.config.copy(
                durationSec = newState.durationSec.coerceIn(60, 60 * 60),
                flashSequence = newState.flashSequence,
            ),
            hasUnsavedChanges = isDirty(it.selectedPreset.name, newState, it.savedProgramName, it.savedTimelineSignature),
            error = null,
        )
    }

    fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
    fun setMonochromeFlashMode(value: Boolean) = _ui.update { it.copy(config = it.config.copy(monochromeFlashMode = value), error = null) }
    fun setCarrier(value: Float) = _ui.update {
        val stableCarrier = value.coerceIn(ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)
        it.copy(config = it.config.copy(carrierFrequencyHz = stableCarrier), error = null)
    }
    fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) }

    fun updateCountdownSeconds(seconds: Int) = viewModelScope.launch { settingsRepository.updateCountdownSeconds(seconds) }
    fun updateForceAudioWithoutHeadphones(value: Boolean) = viewModelScope.launch {
        settingsRepository.updateForceAudioWithoutHeadphones(value)
    }
    fun updateShowImmersiveProgressBar(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveProgressBar(value) }
    fun updatePreferLowestRefreshRate(value: Boolean) = viewModelScope.launch { settingsRepository.updatePreferLowestRefreshRate(value) }
    fun updateMonochromeFlashMode(value: Boolean) = viewModelScope.launch { settingsRepository.updateMonochromeFlashMode(value) }
    fun updateImmersiveBrightnessPercent(value: Int) = viewModelScope.launch {
        settingsRepository.updateImmersiveBrightnessPercent(value)
    }

    fun startSession(): Boolean {
        val state = _ui.value
        if (!(state.settings.safetyAcknowledged && state.settings.safetyAcknowledgedVersion >= SAFETY_VERSION)) {
            _ui.update { it.copy(error = "You must acknowledge safety before starting sessions.") }
            return false
        }
        val validation = SessionValidator.validate(state.config)
        if (validation != null) {
            _ui.update { it.copy(error = validation) }
            return false
        }

        elapsedBeforePauseSec = 0f
        runProgram(startElapsedSec = 0f, includeCountdown = true)
        return true
    }

    fun pause(reason: String? = null) {
        if (_ui.value.runtimeState != RuntimeState.RUNNING) return
        elapsedBeforePauseSec = currentElapsedSec()
        runJob?.cancel()
        audioEngine.stop()
        _ui.update {
            it.copy(
                runtimeState = if (reason == null) RuntimeState.PAUSED else RuntimeState.INTERRUPTED,
                interruptionReason = reason,
            )
        }
    }

    fun resume() {
        val state = _ui.value
        if (state.runtimeState != RuntimeState.PAUSED && state.runtimeState != RuntimeState.INTERRUPTED) return
        runProgram(startElapsedSec = elapsedBeforePauseSec, includeCountdown = true)
    }

    fun stop() {
        runJob?.cancel()
        audioEngine.stop()
        elapsedBeforePauseSec = 0f
        _ui.update { it.copy(runtimeState = RuntimeState.STOPPED, endedEarly = true) }
    }

    fun switchToVisualOnlyAndResume() {
        _ui.update {
            it.copy(
                settings = it.settings.copy(forceAudioWithoutHeadphones = false),
                error = null,
            )
        }
        viewModelScope.launch { settingsRepository.updateForceAudioWithoutHeadphones(false) }
        resume()
    }

    override fun onCleared() {
        audioEngine.stop()
        super.onCleared()
    }

    private fun runProgram(startElapsedSec: Float, includeCountdown: Boolean) {
        runJob?.cancel()
        runJob = viewModelScope.launch {
            val program = _ui.value.timeline
            val durationSec = program.durationSec
            val clampedStartSec = startElapsedSec.coerceIn(0f, durationSec.toFloat())

            if (includeCountdown) {
                val count = _ui.value.settings.countdownSeconds.coerceIn(0, 10)
                if (count > 0) {
                    for (i in count downTo 1) {
                        _ui.update {
                            it.copy(
                                runtimeState = RuntimeState.COUNTDOWN,
                                countdownSec = i,
                                remainingSec = (durationSec - clampedStartSec.toInt()).coerceAtLeast(0),
                                endedEarly = false,
                                interruptionReason = null,
                                runtimeParams = TimelineRuntimeEvaluator.evaluate(program, clampedStartSec),
                            )
                        }
                        delay(1000)
                    }
                }
            }

            val initialParams = TimelineRuntimeEvaluator.evaluate(program, clampedStartSec)
            _ui.update {
                it.copy(
                    runtimeState = RuntimeState.RUNNING,
                    remainingSec = (durationSec - clampedStartSec.toInt()).coerceAtLeast(0),
                    countdownSec = 0,
                    error = null,
                    endedEarly = false,
                    interruptionReason = null,
                    runtimeParams = initialParams,
                )
            }

            var isAudioPlaying = false
            if (shouldPlayAudio(_ui.value.settings, headsetMonitor.isStereoHeadsetAvailable())) {
                audioEngine.start(initialParams.carrierHz, initialParams.binauralHz)
                isAudioPlaying = true
            }

            val tStart = System.currentTimeMillis()
            while (true) {
                delay(50)

                val elapsedSec = clampedStartSec + (System.currentTimeMillis() - tStart) / 1000f
                elapsedBeforePauseSec = elapsedSec.coerceAtMost(durationSec.toFloat())
                val remaining = (durationSec - kotlin.math.ceil(elapsedBeforePauseSec).toInt()).coerceAtLeast(0)
                val currentUi = _ui.value
                val headsetAvailable = headsetMonitor.isStereoHeadsetAvailable()
                val shouldPlayAudioNow = shouldPlayAudio(currentUi.settings, headsetAvailable)

                val params = TimelineRuntimeEvaluator.evaluate(program, elapsedBeforePauseSec)
                _ui.update { it.copy(runtimeParams = params, remainingSec = remaining) }

                if (shouldPlayAudioNow) {
                    if (!isAudioPlaying) {
                        audioEngine.start(params.carrierHz, params.binauralHz)
                        isAudioPlaying = true
                    } else {
                        audioEngine.setFrequencies(params.carrierHz, params.binauralHz)
                    }
                } else if (isAudioPlaying) {
                    audioEngine.stop()
                    isAudioPlaying = false
                }

                if (elapsedBeforePauseSec >= durationSec.toFloat()) break
            }

            audioEngine.stop()
            elapsedBeforePauseSec = 0f
            _ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false, remainingSec = 0) }
        }
    }

    private fun shouldPlayAudio(settings: AppSettings, headsetAvailable: Boolean): Boolean {
        return settings.forceAudioWithoutHeadphones || headsetAvailable
    }

    private fun applyPresetSelection(id: String) {
        val state = _ui.value
        val preset = state.presets.firstOrNull { it.id == id } ?: return

        _ui.update {
            val cfg = preset.toConfig(SessionMode.AUDIO_VISUAL)
            val timeline = timelineForProgramId(preset.id, cachedPrograms)
            val savedSignature = timelineProgramSignature(timeline)
            it.copy(
                selectedPreset = preset,
                config = cfg.copy(
                    durationSec = cfg.durationSec.coerceIn(60, 60 * 60),
                    flashSequence = timeline.flashSequence,
                ),
                timeline = timeline,
                savedProgramName = preset.name,
                savedTimelineSignature = savedSignature,
                hasUnsavedChanges = false,
                error = null,
            )
        }
        elapsedBeforePauseSec = 0f

        viewModelScope.launch {
            settingsRepository.updateLastPreset(id)
        }
    }

    private fun currentElapsedSec(): Float {
        val state = _ui.value
        val total = state.timeline.durationSec.toFloat().coerceAtLeast(1f)
        return (total - state.remainingSec).coerceIn(0f, total)
    }

    private fun timelineForProgramId(programId: String, programs: List<UserProgramEntity>): TimelineEditorState {
        val fromSaved = programs.firstOrNull { it.id == programId }?.timeline
        if (fromSaved != null) return fromSaved

        val builtInPreset = Presets.builtIn.firstOrNull { it.id == programId }
        return if (builtInPreset != null) TimelineProgramFactory.fromPreset(builtInPreset) else TimelineEditorState.default()
    }

    private fun nextSortOrder(): Int = (cachedPrograms.maxOfOrNull { it.sortOrder } ?: 999) + 1

    private fun nextUntitledName(): String {
        val names = cachedPrograms.map { it.name.lowercase() }.toSet()
        var i = 1
        while (true) {
            val candidate = "Custom $i"
            if (candidate.lowercase() !in names) return candidate
            i++
        }
    }

    class Factory(
        private val settingsRepository: SettingsRepository,
        private val userProgramRepository: UserProgramRepository,
        private val headsetMonitor: HeadsetMonitor,
        private val audioEngine: BinauralAudioEngine,
        private val billingManager: solutions.tretter.mindmachine.billing.BillingManager,
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T =
            MainViewModel(settingsRepository, userProgramRepository, headsetMonitor, audioEngine, billingManager) as T
    }
}
