package dev.privacyllc.period.feature.export

import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.core.datastore.UserPreferencesRepository
import dev.privacyllc.period.core.export.ExportDocument
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.time.Clock
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Writes the export straight into the document the user chose.
 *
 * ## There is never a second copy
 *
 * `#35`'s verify line asks for "no copy left in app-private or external storage
 * afterwards", and the way to satisfy that is to never make one. The Storage
 * Access Framework hands back a writable `content://` URI for the document the
 * user picked, so the bytes go from memory to their file. The alternative —
 * write to `cacheDir`, share it with a `FileProvider`, delete it after — creates
 * exactly the temporary file the issue warns about, and its deletion races the
 * receiving app still reading it.
 *
 * `takePersistableUriPermission` is deliberately **not** called. A persisted
 * write grant to a file of cycle dates is a capability that outlives the reason
 * for it.
 *
 * ## Nothing here logs
 *
 * `app` is in `modulesSeeingHealthData`, and this is the one function in the
 * project that holds every record as a single string. `print(` and `println(`
 * are matched as substrings by the guard, which is why `PrintWriter` is absent
 * too. Failures are reported as states, never as messages carrying a cause.
 */
@Singleton
class DataExporter @Inject constructor(
    private val repository: CycleRepository,
    private val preferences: UserPreferencesRepository,
    private val controller: ExportController,
    private val clock: Clock,
) {

    /**
     * Its own scope, living as long as the process.
     *
     * Not `viewModelScope`: leaving the export screen — or the app re-locking
     * while a document provider is still finishing — would cancel the write
     * halfway and leave a truncated file at a destination the user believes
     * holds their history.
     *
     * The handler is a backstop and is empty for the reason `PeriodApplication`
     * gives: nothing on this path may be logged, and an exception here would
     * carry the record it failed on. Failures are caught explicitly below and
     * reported as state.
     */
    private val scope = CoroutineScope(
        SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { _, _ -> },
    )

    /**
     * Serialise everything, then write once.
     *
     * Built fully in memory before the stream is opened, so a failure while
     * reading the database cannot leave a half-written file at a destination the
     * user believes now holds their history. A realistic archive is tens of
     * kilobytes; there is nothing to stream.
     */
    fun export(context: Context, uri: Uri) {
        controller.onWriting()
        scope.launch {
            var wroteAnything = false
            try {
                val periods = repository.allPeriods.first()
                val spotting = repository.spotting.first()
                val settings = preferences.preferences.first().toExportedSettings()

                val document = ExportDocument.render(
                    periods = periods,
                    spotting = spotting,
                    settings = settings,
                    appVersion = installedVersionName(context),
                    exportedOn = LocalDate.now(clock),
                )

                // "wt" truncates. The picker has already created the document,
                // and without it a shorter export would leave the tail of a
                // previous one behind.
                val stream = context.contentResolver.openOutputStream(uri, "wt")
                    ?: throw IllegalStateException("the document provider returned no stream")

                stream.use { out ->
                    wroteAnything = true
                    out.write(document.toByteArray(Charsets.UTF_8))
                    out.flush()
                }
                // Declared only after `use` returned: it rethrows a failure from
                // close(), and a buffered write that fails on flush would
                // otherwise be reported as a complete export.
                controller.onDone()
            } catch (failure: Exception) {
                // ACTION_CREATE_DOCUMENT already created a zero-byte file at the
                // destination before this ran, so a failure always leaves
                // something behind unless it is removed.
                val removed = runCatching {
                    DocumentsContract.deleteDocument(context.contentResolver, uri)
                }.getOrDefault(false)

                controller.onFailed(partialFileLeft = !removed && wroteAnything)
            }
        }
    }
}
