package pl.firmatpp.kierowca.ui

import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDate
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
import pl.firmatpp.kierowca.data.model.DriverDto
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
import pl.firmatpp.kierowca.data.upload.PhotoUploadOutbox
import pl.firmatpp.kierowca.sync.DriverLiveSyncClient
import pl.firmatpp.kierowca.sync.DriverSyncHint
import pl.firmatpp.kierowca.sync.DriverSyncWorker

enum class DriverScreen { Initializing, Phone, Otp, Routes, Profile, Detail, Photo, PhotoQueue }

data class DriverUiState(
    val screen: DriverScreen = DriverScreen.Initializing,
    val loading: Boolean = true,
    val refreshing: Boolean = false,
    val phone: String = "",
    val otpCode: String = "",
    val maskedPhone: String = "",
    val driver: DriverDto? = null,
    val routes: List<DriverRouteDto> = emptyList(),
    val selectedDate: String = LocalDate.now().toString(),
    val minRouteDate: String = LocalDate.now().minusDays(7).toString(),
    val maxRouteDate: String = LocalDate.now().plusDays(1).toString(),
    val autoRefreshSeconds: Int = 60,
    val allowGalleryUploads: Boolean = true,
    val allowRouteCompletion: Boolean = false,
    val requirePreciseLocationForPhotos: Boolean = false,
    val selectedRoute: DriverRouteDto? = null,
    val selectedPhoto: RoutePhotoDto? = null,
    val photoUploads: List<PhotoUploadEntity> = emptyList(),
    val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
    val deletingPhotoIds: Set<String> = emptySet(),
    val completingRoute: Boolean = false,
    val imageAuthHeader: String? = null,
    val isOnline: Boolean = true,
    val isStale: Boolean = false,
    val lastSuccessfulSyncAtEpochMillis: Long? = null,
    val feedback: String? = null,
    val error: String? = null,
)

class DriverViewModel(application: Application) : AndroidViewModel(application) {
    private val repository = DriverRepository(application)
    private val syncRepository = DriverSyncRepository(application, repository)
    private val networkMonitor = NetworkMonitor(application)
    private val photoUploadOutbox = PhotoUploadOutbox(application)
    private val _state = MutableStateFlow(DriverUiState(loading = true))
    private val otpAutoSubmitPolicy = OtpAutoSubmitPolicy()
    private val liveSyncClient = DriverLiveSyncClient(
        repository = repository,
        onConnected = { viewModelScope.launch { checkRemoteSyncState() } },
        onHint = { hint -> viewModelScope.launch { handleSyncHint(hint) } },
    )
    private var photoUploadsJob: Job? = null
    private var pushTokenRegisteredForDriverId: String? = null
    val state: StateFlow<DriverUiState> = _state

    init {
        viewModelScope.launch {
            photoUploadOutbox.observeQueuedUploads().collect { uploads ->
                _state.update { it.copy(queuedPhotoUploads = uploads) }
            }
        }
        viewModelScope.launch {
            networkMonitor.isOnline.collect { online ->
                _state.update {
                    it.copy(
                        isOnline = online,
                        isStale = if (!online && it.lastSuccessfulSyncAtEpochMillis != null) true else it.isStale,
                    )
                }
                if (online && repository.hasToken()) {
                    refreshCurrentScopeFromSyncState()
                }
            }
        }
        viewModelScope.launch {
            if (repository.hasToken()) {
                refreshRoutes()
            } else {
                _state.update { it.copy(screen = DriverScreen.Phone, loading = false) }
            }
        }
    }

    fun requestOtp(phone: String) = runLoading {
        val response = repository.requestOtp(phone)
        otpAutoSubmitPolicy.reset()
        _state.update {
            it.copy(
                screen = DriverScreen.Otp,
                phone = phone,
                otpCode = "",
                maskedPhone = response.phoneMasked.orEmpty(),
                error = null,
            )
        }
    }

    fun verifyOtp(code: String) = runLoading {
        val driver = repository.verifyOtp(_state.value.phone, code, android.os.Build.MODEL ?: "Android")
        _state.update { it.copy(driver = driver, imageAuthHeader = repository.imageAuthHeader()) }
        refreshRoutes()
    }

    fun updateOtpCode(code: String) {
        val sanitized = code.filter(Char::isDigit).take(6)
        _state.update { it.copy(otpCode = sanitized) }

        if (otpAutoSubmitPolicy.shouldSubmit(sanitized, _state.value.loading)) {
            verifyOtp(sanitized)
        }
    }

    fun refreshRoutes() = loadRoutes(date = _state.value.selectedDate, showLoading = true, navigateToRoutes = true)

    fun refreshRoutesSilently() = loadRoutes(date = _state.value.selectedDate, showLoading = false, navigateToRoutes = false)

    fun selectRouteDate(date: String) = loadRoutes(date = date, showLoading = true, navigateToRoutes = true)

    private fun loadRoutes(date: String?, showLoading: Boolean, navigateToRoutes: Boolean) {
        viewModelScope.launch {
            _state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) }

            runCatching {
                val cached = syncRepository.bootstrap(date)
                val response = cached.value
                val settings = response.driverAppSettings
                _state.update {
                    it.copy(
                        screen = if (navigateToRoutes) DriverScreen.Routes else it.screen,
                        driver = response.session.driver,
                        routes = response.routes.today,
                        selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
                        minRouteDate = settings?.minDate ?: it.minRouteDate,
                        maxRouteDate = settings?.maxDate ?: it.maxRouteDate,
                        autoRefreshSeconds = settings?.autoRefreshSeconds ?: it.autoRefreshSeconds,
                        allowGalleryUploads = settings?.allowGalleryUploads ?: it.allowGalleryUploads,
                        allowRouteCompletion = settings?.allowRouteCompletion ?: it.allowRouteCompletion,
                        requirePreciseLocationForPhotos = settings?.requirePreciseLocationForPhotos
                            ?: it.requirePreciseLocationForPhotos,
                        imageAuthHeader = repository.imageAuthHeader(),
                        isStale = cached.stale || !it.isOnline,
                        lastSuccessfulSyncAtEpochMillis = cached.syncedAtEpochMillis ?: it.lastSuccessfulSyncAtEpochMillis,
                        error = null,
                    )
                }
                response.session.driver.id.let { driverId ->
                    liveSyncClient.start(driverId)
                    registerPushTokenIfAvailable(driverId)
                }
            }.onFailure { throwable ->
                _state.update {
                    val apiError = ApiErrorMapper.map(throwable)
                    it.copy(
                        screen = when {
                            apiError.kind == ApiErrorKind.Auth -> DriverScreen.Phone
                            it.screen == DriverScreen.Initializing -> DriverScreen.Routes
                            else -> it.screen
                        },
                        isStale = it.lastSuccessfulSyncAtEpochMillis != null,
                        error = apiError.message,
                    )
                }
            }

            _state.update {
                it.copy(loading = false, refreshing = false)
            }
        }
    }

    fun openRoute(routeId: String) = runLoading {
        val cached = syncRepository.route(routeId)
        val response = cached.value
        photoUploadOutbox.discardConfirmedServerPhotos(response.route)
        _state.update {
            it.copy(
                screen = DriverScreen.Detail,
                selectedRoute = response.route,
                feedback = null,
                isStale = cached.stale || !it.isOnline,
                lastSuccessfulSyncAtEpochMillis = cached.syncedAtEpochMillis ?: it.lastSuccessfulSyncAtEpochMillis,
                error = null,
            )
        }
        observePhotoUploads(routeId)
    }

    fun refreshSelectedRoute() {
        val routeId = _state.value.selectedRoute?.id ?: return

        viewModelScope.launch {
            _state.update { it.copy(refreshing = true, feedback = null, error = null) }

            runCatching { syncRepository.route(routeId) }
                .onSuccess { cached ->
                    val response = cached.value
                    photoUploadOutbox.discardConfirmedServerPhotos(response.route)
                    _state.update {
                        it.copy(
                            selectedRoute = response.route,
                            routes = it.routes.map { route -> if (route.id == routeId) response.route else route },
                            isStale = cached.stale || !it.isOnline,
                            lastSuccessfulSyncAtEpochMillis = cached.syncedAtEpochMillis ?: it.lastSuccessfulSyncAtEpochMillis,
                            error = null,
                        )
                    }
                }
                .onFailure { throwable ->
                    _state.update { it.copy(error = throwable.message ?: "Wystapil blad.") }
                }

            _state.update { it.copy(refreshing = false) }
        }
    }

    fun openProfile() {
        _state.update { it.copy(screen = DriverScreen.Profile, error = null) }
    }

    fun openPhotoQueue() {
        _state.update { it.copy(screen = DriverScreen.PhotoQueue, error = null) }
    }

    fun uploadPhoto(uri: Uri, source: String, metadata: PhotoUploadMetadata = PhotoUploadMetadata()) {
        val route = _state.value.selectedRoute ?: return
        viewModelScope.launch {
            _state.update { it.copy(error = null) }
            runCatching { photoUploadOutbox.enqueue(route.id, uri, source, metadata) }
                .onFailure { throwable ->
                    _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
                }
        }
    }

    fun retryPhotoUpload(upload: PhotoUploadEntity) {
        viewModelScope.launch {
            runCatching { photoUploadOutbox.retry(upload.clientRequestId) }
                .onFailure { throwable ->
                    _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
                }
        }
    }

    fun deletePhoto(photo: RoutePhotoDto) {
        val route = _state.value.selectedRoute ?: return
        viewModelScope.launch {
            _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds + photo.id, error = null) }
            runCatching {
                repository.deletePhoto(photo.id)
                val response = syncRepository.route(route.id).value
                photoUploadOutbox.discardConfirmedServerPhotos(response.route)
                _state.update {
                    it.copy(
                        selectedRoute = response.route,
                        routes = it.routes.map { item -> if (item.id == route.id) response.route else item },
                        error = null,
                    )
                }
            }.onFailure { throwable ->
                _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
            }
            _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds - photo.id) }
        }
    }

    fun deleteConfirmedUpload(upload: PhotoUploadEntity) {
        val route = _state.value.selectedRoute ?: return
        val serverPhotoId = confirmedUploadServerPhotoId(upload.status, upload.serverPhotoId) ?: return

        viewModelScope.launch {
            _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds + serverPhotoId, error = null) }
            runCatching {
                repository.deletePhoto(serverPhotoId)
                photoUploadOutbox.discard(upload.clientRequestId)
                val response = syncRepository.route(route.id).value
                photoUploadOutbox.discardConfirmedServerPhotos(response.route)
                _state.update {
                    it.copy(
                        selectedRoute = response.route,
                        routes = it.routes.map { item -> if (item.id == route.id) response.route else item },
                        error = null,
                    )
                }
            }.onFailure { throwable ->
                _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
            }
            _state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds - serverPhotoId) }
        }
    }

    fun completeSelectedRoute() {
        val route = _state.value.selectedRoute ?: return
        if (_state.value.completingRoute || !_state.value.isOnline) return

        viewModelScope.launch {
            _state.update { it.copy(completingRoute = true, feedback = null, error = null) }
            runCatching {
                val completed = repository.completeRoute(route.id).route
                syncRepository.cacheConfirmedRoute(completed, _state.value.selectedDate)
                val refreshed = runCatching { syncRepository.route(route.id) }
                    .getOrNull()
                    ?.takeUnless { it.stale }
                    ?.value
                    ?.route
                    ?: completed
                photoUploadOutbox.discardConfirmedServerPhotos(refreshed)
                _state.update {
                    it.copy(
                        selectedRoute = refreshed,
                        routes = it.routes.map { item -> if (item.id == route.id) refreshed else item },
                        feedback = "Kurs zakończony",
                        error = null,
                    )
                }
            }.onFailure { throwable ->
                _state.update { it.copy(feedback = null, error = ApiErrorMapper.map(throwable).message) }
            }
            _state.update { it.copy(completingRoute = false) }
        }
    }

    fun openPhoto(photo: RoutePhotoDto) {
        _state.update { it.copy(screen = DriverScreen.Photo, selectedPhoto = photo) }
    }

    fun back() {
        _state.update {
            when (it.screen) {
                DriverScreen.Photo -> it.copy(screen = DriverScreen.Detail, selectedPhoto = null)
                DriverScreen.PhotoQueue -> it.copy(screen = DriverScreen.Routes)
                DriverScreen.Detail -> {
                    photoUploadsJob?.cancel()
                    it.copy(screen = DriverScreen.Routes, selectedRoute = null, photoUploads = emptyList(), feedback = null)
                }
                DriverScreen.Profile -> it.copy(screen = DriverScreen.Routes)
                DriverScreen.Otp -> it.copy(screen = DriverScreen.Phone)
                else -> it
            }
        }
    }

    fun logout() = runLoading {
        photoUploadsJob?.cancel()
        liveSyncClient.stop()
        repository.logout()
        syncRepository.clearCache()
        pushTokenRegisteredForDriverId = null
        _state.update { DriverUiState(screen = DriverScreen.Phone, loading = false) }
    }

    private fun runLoading(block: suspend () -> Unit) {
        viewModelScope.launch {
            _state.update { it.copy(loading = true, error = null) }
            runCatching { block() }
                .onFailure { throwable ->
                    _state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
                }
            _state.update { it.copy(loading = false) }
        }
    }

    private fun observePhotoUploads(routeId: String) {
        photoUploadsJob?.cancel()
        photoUploadsJob = viewModelScope.launch {
            photoUploadOutbox.observeVisibleForRoute(routeId).collect { uploads ->
                _state.update { it.copy(photoUploads = uploads) }
            }
        }
    }

    private fun refreshCurrentScopeFromSyncState() {
        val snapshot = _state.value
        if (snapshot.screen == DriverScreen.Routes) {
            refreshRoutesSilently()
            return
        }
        if (snapshot.screen == DriverScreen.Detail) {
            refreshSelectedRoute()
        }
    }

    private suspend fun checkRemoteSyncState() {
        val snapshot = _state.value
        val routeId = snapshot.selectedRoute?.id
        val remote = runCatching { syncRepository.fetchSyncState(snapshot.selectedDate, routeId) }.getOrNull() ?: return
        val needsRefresh = remote.scopes.any { syncRepository.shouldRefresh(it) }

        if (needsRefresh) {
            refreshCurrentScopeFromSyncState()
        } else {
            syncRepository.saveSyncStates(remote)
        }
    }

    private suspend fun handleSyncHint(hint: DriverSyncHint) {
        if (!syncRepository.shouldRefresh(hint.asScope())) return

        val snapshot = _state.value
        when (hint.scope) {
            DriverSyncRepository.SCOPE_ROUTES -> {
                if (hint.date == null || hint.date == snapshot.selectedDate) {
                    refreshRoutesSilently()
                } else {
                    DriverSyncWorker.enqueue(getApplication(), hint.date, null)
                }
            }
            DriverSyncRepository.SCOPE_ROUTE_DETAIL -> {
                if (hint.routeId == snapshot.selectedRoute?.id) {
                    refreshSelectedRoute()
                } else {
                    DriverSyncWorker.enqueue(getApplication(), hint.date, hint.routeId)
                }
            }
            DriverSyncRepository.SCOPE_SETTINGS -> refreshRoutesSilently()
        }
    }

    private fun registerPushTokenIfAvailable(driverId: String) {
        if (pushTokenRegisteredForDriverId == driverId) return

        runCatching {
            FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
                viewModelScope.launch {
                    runCatching {
                        repository.storePushToken(token)
                        pushTokenRegisteredForDriverId = driverId
                    }
                }
            }
        }
    }

    override fun onCleared() {
        liveSyncClient.close()
        super.onCleared()
    }
}
