package com.doctate.watch.audio

import android.util.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.json.JSONObject

/**
 * Persistent FIFO queue of recordings waiting to be uploaded. One pair per
 * recording: `{stem}.m4a` + `{stem}.meta.json`. The audio is written by
 * [AudioRecorder] directly into [dir]; the sidecar is written here, atomic
 * tmp+rename so a crash mid-write cannot leave a partial file.
 *
 * Mirrors `doctate-desktop::upload` exactly (sidecar shape, scan rules,
 * orphan handling). Wire-compatible with the desktop client.
 */
class PendingStore(private val dir: File) {

    /** Compute the target audio path for a new recording. Caller passes it to [AudioRecorder.start]. */
    fun audioPathFor(caseId: String, recordedAtUtc: String): File {
        if (!dir.exists()) dir.mkdirs()
        val stem = "${caseId}_${recordedAtToFilenameStem(recordedAtUtc)}"
        return File(dir, "$stem.m4a")
    }

    /** Sidecar path next to a `.m4a` — same dir, same stem, `.meta.json`. */
    fun sidecarPathFor(audio: File): File =
        File(audio.parentFile ?: dir, "${audio.nameWithoutExtension}.meta.json")

    /**
     * Persist a sidecar atomically. Caller must have already written the
     * `.m4a` audio to [PendingUpload.file]; the sidecar makes the pair
     * complete and visible to [scan].
     */
    fun writeSidecar(upload: PendingUpload) {
        if (!dir.exists()) dir.mkdirs()
        val sidecar = sidecarPathFor(upload.file)
        val tmp = File(sidecar.parentFile ?: dir, "${sidecar.name}.tmp")
        val json = JSONObject().apply {
            put("case_id", upload.caseId)
            put("recorded_at", upload.recordedAt)
        }.toString(2)
        tmp.writeText(json, Charsets.UTF_8)
        Files.move(
            tmp.toPath(),
            sidecar.toPath(),
            StandardCopyOption.REPLACE_EXISTING,
            StandardCopyOption.ATOMIC_MOVE,
        )
    }

    /**
     * Scan [dir] for `.m4a` files with a matching sidecar. Returns the
     * complete pairs sorted by `recordedAt` ascending — the upload worker
     * processes them FIFO so the oldest dictation wins. Orphans (m4a
     * without sidecar, unreadable sidecar) are logged and skipped — one
     * broken pair must not stop recovery of the rest.
     */
    fun scan(): List<PendingUpload> {
        if (!dir.exists()) return emptyList()
        val files = dir.listFiles { f -> f.isFile && f.name.endsWith(".m4a") } ?: return emptyList()
        val out = mutableListOf<PendingUpload>()
        for (audio in files) {
            val sidecar = sidecarPathFor(audio)
            if (!sidecar.exists()) continue
            try {
                val obj = JSONObject(sidecar.readText(Charsets.UTF_8))
                out += PendingUpload(
                    caseId = obj.getString("case_id"),
                    recordedAt = obj.getString("recorded_at"),
                    file = audio,
                )
            } catch (e: Exception) {
                Log.w(TAG, "skipping unreadable sidecar ${sidecar.name}: ${e.message}")
            }
        }
        return out.sortedBy { it.recordedAt }
    }

    /**
     * Drop both files of a pair. Idempotent — missing files are not an
     * error. Called by the sync worker after a Success or Terminal ACK.
     */
    fun deletePair(audio: File) {
        val sidecar = sidecarPathFor(audio)
        if (audio.exists() && !audio.delete()) {
            Log.w(TAG, "failed to delete audio ${audio.name}")
        }
        if (sidecar.exists() && !sidecar.delete()) {
            Log.w(TAG, "failed to delete sidecar ${sidecar.name}")
        }
    }

    companion object {
        private const val TAG = "PendingStore"

        /** RFC3339 → filesystem-safe stem (replace `:` with `-`). 1:1 to
         *  `doctate-common::timestamp::recorded_at_to_filename_stem`. */
        fun recordedAtToFilenameStem(recordedAt: String): String =
            recordedAt.replace(':', '-')

        /** Current UTC time as RFC3339 with second granularity, e.g.
         *  `2026-04-15T10:32:00Z`. 1:1 to
         *  `doctate-common::timestamp::now_rfc3339`. The truncation is
         *  load-bearing: the server's filename parser expects exactly
         *  `YYYY-MM-DDTHH-MM-SSZ` after the `:` → `-` substitution, and
         *  `Instant.now().toString()` would otherwise leak microseconds
         *  from the platform clock. */
        fun nowRfc3339(): String =
            Instant.now().truncatedTo(ChronoUnit.SECONDS).toString()
    }
}
