package dev.privacyllc.period.core.export

import dev.privacyllc.period.domain.cycle.PeriodRecord
import dev.privacyllc.period.domain.cycle.SpottingRecord
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter

/**
 * The settings that are the user's own, as this module needs them.
 *
 * Its own type rather than `UserPreferences`, deliberately. Reaching for that
 * class would put `core/datastore` on this module's classpath and make the
 * export inherit every field anybody ever adds to it — including fields that
 * must never leave, like the Play entitlement. Mapping is explicit and total,
 * in `:app`, so adding a preference is a compile error here until somebody
 * decides whether it belongs in an archive.
 */
data class ExportedSettings(
    val notificationPrivacy: String,
    val reminderTime: LocalTime,
    val periodApproaching: Boolean,
    val periodExpectedToday: Boolean,
    val didItStart: Boolean,
    val periodEndCheckIn: Boolean,
    val fertileWindow: Boolean,
    val ovulation: Boolean,
    val biometricUnlock: Boolean,
    val theme: String,
) {
    /**
     * Dateless and valueless, for the reason `PeriodRecord.toString` gives: a
     * data class renders its own contents into any exception message that
     * interpolates it, and `checkNoHealthLogging` cannot see that happen.
     * A reminder time is a fact about the user's day.
     */
    override fun toString(): String = "ExportedSettings(privacy=$notificationPrivacy)"
}

/**
 * The file the user gets, and the decision that outlives this batch.
 *
 * Whatever ships first is what people's archives are in, so the shape is a
 * contract rather than an implementation detail. It is pinned byte-for-byte by
 * `ExportFormatGoldenTest` against a committed golden file, which doubles as
 * the documented example — `SECURITY.md` links at it rather than pasting a
 * second copy that can drift.
 *
 * ## The contract
 *
 * 1. `format` is a magic string. Anything else is somebody else's JSON, and a
 *    future importer refuses it rather than guessing.
 * 2. `formatVersion` is an integer, and readers must ignore unknown keys — so
 *    keys may be **added** freely. Removing one, retyping one, or changing what
 *    one means requires a version bump.
 * 3. Every key is always present. Missing information is `null`, never an
 *    absent key, so a reader never has to distinguish "not recorded" from
 *    "written by an older version".
 * 4. Arrays ascend by their natural key, matching the DAO ordering, so two
 *    exports diff cleanly and a person can see they lost nothing.
 * 5. Natural keys, not row ids. Both tables carry a UNIQUE index on their date,
 *    so ids add nothing — and autoGenerate ids are monotonic, so gaps would
 *    disclose how many records the user **deleted**.
 * 6. Enums travel as their Kotlin `name`, uppercase.
 * 7. Keys are English and never localised. Only the `aboutThisFile` prose may
 *    ever be translated.
 * 8. Dates are `uuuu-MM-dd` with **no timezone and no conversion, ever**.
 *
 * ## Why the JSON is written by hand
 *
 * Because nothing free-form reaches it. Dates are formatted, enums are `name`s,
 * booleans are booleans, and [requireSafe] refuses any value outside a narrow
 * character set — so there is no string to escape and no escaping bug to have.
 * That is a stronger guarantee than a serializer would give, and it costs no
 * dependency in a project that adds them reluctantly.
 *
 * `print(` and `println(` are in `checkNoHealthLogging`'s forbidden list as
 * substrings, so `PrintWriter.print`/`println` fail the build. Nothing here
 * needs them; the file is built as a string and handed back.
 */
object ExportDocument {

    const val FORMAT = "privacy-period-tracker-export"
    const val FORMAT_VERSION = 1

    private val DATE = DateTimeFormatter.ISO_LOCAL_DATE
    private val TIME = DateTimeFormatter.ofPattern("HH:mm")

    /**
     * The only characters any emitted value may contain.
     *
     * Every value that is not a date, a boolean or an integer passes through
     * here. An enum constant renamed to something with a quote or a backslash
     * in it would otherwise produce a file that is not JSON, and the failure
     * would appear in the user's archive rather than in a test.
     */
    private val SAFE = Regex("""^[A-Za-z0-9_:.\-]+$""")

    private fun requireSafe(value: String): String {
        require(SAFE.matches(value)) { "a value outside the safe character set reached the export" }
        return value
    }

    /**
     * What the file says about itself, in the second person.
     *
     * Prose explains; it never restates a value that also appears as data, so
     * the two halves cannot disagree. The line about encryption is the one that
     * has to be there: this file leaves the app's sandbox and the app lock has
     * no reach over it.
     */
    private val ABOUT = listOf(
        "This is your own record, exported from the period tracker you use on your phone.",
        "It holds every period and spotting entry you logged, and your settings. " +
            "Nothing else: no predictions, no averages, nothing about your phone, " +
            "and nothing that was ever sent anywhere.",
        "A date like 2026-07-16 is a calendar date, exactly as you entered it. " +
            "It carries no time zone and is never shifted, wherever in the world you open this file.",
        "Your PIN is not in this file and cannot be.",
        "This file is not encrypted. Anyone who opens it can read it, so keep it somewhere you trust.",
        "Keep this file. A future version of the app will be able to read it back.",
    )

    fun render(
        periods: List<PeriodRecord>,
        spotting: List<SpottingRecord>,
        settings: ExportedSettings,
        appVersion: String,
        exportedOn: LocalDate,
    ): String {
        val b = StringBuilder()
        b.append("{\n")
        b.append("""  "format": "${requireSafe(FORMAT)}",""").append('\n')
        b.append("""  "formatVersion": $FORMAT_VERSION,""").append('\n')

        b.append("""  "aboutThisFile": [""").append('\n')
        ABOUT.forEachIndexed { i, line ->
            b.append("""    "$line"""").append(if (i == ABOUT.lastIndex) "\n" else ",\n")
        }
        b.append("  ],\n")

        b.append("""  "exportedOn": "${exportedOn.format(DATE)}",""").append('\n')
        b.append("""  "appVersion": "${requireSafe(appVersion)}",""").append('\n')

        // Rule 4: ascending by natural key, matching the DAO's ORDER BY.
        b.append("""  "periods": [""").append('\n')
        val sortedPeriods = periods.sortedBy { it.startDate }
        sortedPeriods.forEachIndexed { i, p ->
            b.append("    {\n")
            b.append("""      "startDate": "${p.startDate.format(DATE)}",""").append('\n')
            b.append("""      "endDate": ${p.endDate?.let { "\"${it.format(DATE)}\"" } ?: "null"},""").append('\n')
            b.append("""      "source": "${requireSafe(p.source.name)}",""").append('\n')
            b.append("""      "confirmed": ${p.isConfirmed}""").append('\n')
            b.append("    }").append(if (i == sortedPeriods.lastIndex) "\n" else ",\n")
        }
        b.append("  ],\n")

        b.append("""  "spotting": [""").append('\n')
        val sortedSpotting = spotting.sortedBy { it.date }
        sortedSpotting.forEachIndexed { i, s ->
            b.append("    {\n")
            b.append("""      "date": "${s.date.format(DATE)}"""").append('\n')
            b.append("    }").append(if (i == sortedSpotting.lastIndex) "\n" else ",\n")
        }
        b.append("  ],\n")

        b.append("""  "settings": {""").append('\n')
        b.append("""    "notificationPrivacy": "${requireSafe(settings.notificationPrivacy)}",""").append('\n')
        b.append("""    "reminderTime": "${settings.reminderTime.format(TIME)}",""").append('\n')
        b.append("""    "reminders": {""").append('\n')
        b.append("""      "periodApproaching": ${settings.periodApproaching},""").append('\n')
        b.append("""      "periodExpectedToday": ${settings.periodExpectedToday},""").append('\n')
        b.append("""      "didItStart": ${settings.didItStart},""").append('\n')
        b.append("""      "periodEndCheckIn": ${settings.periodEndCheckIn},""").append('\n')
        b.append("""      "fertileWindow": ${settings.fertileWindow},""").append('\n')
        b.append("""      "ovulation": ${settings.ovulation}""").append('\n')
        b.append("    },\n")
        b.append("""    "biometricUnlock": ${settings.biometricUnlock},""").append('\n')
        b.append("""    "theme": "${requireSafe(settings.theme)}"""").append('\n')
        b.append("  }\n")

        b.append("}\n")
        return b.toString()
    }
}
