package com.keylightpiano.pianotimeline.data

import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.keylightpiano.pianotimeline.data.local.AppDatabase
import com.keylightpiano.pianotimeline.data.local.RecordingDao
import com.keylightpiano.pianotimeline.data.local.toEntity
import com.keylightpiano.pianotimeline.data.remote.MusicNetwork
import com.keylightpiano.pianotimeline.domain.Recording
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import java.io.File

/**
 * DataStore singleton: one per app, declared as a top-level extension property.
 * This is the recommended pattern from the official docs.
 * The string "settings" becomes the filename: settings.preferences_pb
 */
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

private val KEY_REVISION = intPreferencesKey("cached_revision")

private const val TAG = "SyncManager"

/**
 * Handles the startup sync:
 *
 * 1. Fetch list.json from the server.
 * 2. Compare its revision against the locally stored one.
 * 3. If unchanged → skip, the cache is current.
 * 4. If changed →
 *    a. Diff IDs: find new recordings, removed recordings.
 *    b. Diff file-sizes: find recordings whose audio file changed on the server.
 *    c. Insert new entries into Room (with no cached paths).
 *    d. Delete removed entries from Room AND delete their cached files from disk.
 *    e. For changed-size entries: delete the stale cached files, clear their paths in Room.
 *    f. Update all remaining entries' metadata (artist, title, duration, etc. might have changed).
 *    g. Store the new revision number.
 *
 * The SyncManager does NOT download audio/midi files. Those are fetched on-demand
 * when the user plays a recording (handled by the player layer later).
 */
class SyncManager(private val context: Context) {

    private val dao: RecordingDao = AppDatabase.getInstance(context).recordingDao()

    /**
     * Result of a sync attempt.
     */
    sealed interface SyncResult {
        /** Cache is already up to date. */
        data object UpToDate : SyncResult
        /** Cache was refreshed. Contains the updated recordings for immediate use. */
        data class Updated(
            val added: Int,
            val removed: Int,
            val sizeChanged: Int
        ) : SyncResult
        /** Network or parsing failure. Stale cache may still be usable. */
        data class Failed(val message: String) : SyncResult
    }

    /**
     * Run the full sync. Safe to call from any coroutine — does its own dispatching.
     */
    suspend fun sync(): SyncResult = withContext(Dispatchers.IO) {
        try {
            // 1. Fetch fresh list from server
            val dto = MusicNetwork.api.getRecordingList()
            val serverRevision = dto.revision
            val serverRecordings = dto.data.mapNotNull { it.toDomainOrNull() }

            // 2. Read our last-known revision
            val prefs = context.dataStore.data.first()
            val cachedRevision = prefs[KEY_REVISION] ?: -1

            // Short-circuit only if the revision matches AND we actually have data.
            // The dao.count() guard protects against a desync where the stored
            // revision is current but the DB was wiped — without it, an empty cache
            // with a matching revision would never repopulate.
            if (serverRevision == cachedRevision && dao.count() > 0) {
                Log.d(TAG, "Revision $serverRevision unchanged — cache is current")
                return@withContext SyncResult.UpToDate
            }

            Log.d(TAG, "Revision changed: $cachedRevision → $serverRevision (or empty cache) — syncing")

            // 3. Build lookup maps for diffing
            val serverById = serverRecordings.associateBy { it.id }
            val cachedIds = dao.getAllIds().toSet()
            val serverIds = serverById.keys

            // First sync = the cache was actually empty before this run.
            // We derive this from the DB itself, NOT the stored revision, because
            // the DB and the stored revision can fall out of step (e.g. the DB gets
            // wiped by a destructive migration while DataStore keeps the old revision).
            // Relying on the revision caused every recording to be flagged "new" on
            // a fresh install where a stale revision lingered.
            val isFirstSync = cachedIds.isEmpty()

            // 4a. New recordings (on server but not in cache)
            val addedIds = serverIds - cachedIds
            // 4b. Removed recordings (in cache but not on server)
            val removedIds = cachedIds - serverIds
            // 4c. Recordings that exist in both — check for file-size changes
            val commonIds = cachedIds.intersect(serverIds)

            val cachedSizes = dao.getIdAndSizes().associate { it.id to it.fileSize }
            val sizeChangedIds = commonIds.filter { id ->
                val serverSize = serverById[id]?.fileSize ?: 0L
                val cachedSize = cachedSizes[id] ?: 0L
                serverSize != cachedSize
            }.toSet()

            // 5. Delete removed entries and their files
            if (removedIds.isNotEmpty()) {
                deleteFilesForIds(removedIds)
                dao.deleteByIds(removedIds)
                Log.d(TAG, "Removed ${removedIds.size} deleted recordings")
            }

            // 6. Clear cached files for size-changed entries
            for (id in sizeChangedIds) {
                deleteFilesForId(id)
                dao.clearCachedPaths(id)
            }
            if (sizeChangedIds.isNotEmpty()) {
                Log.d(TAG, "${sizeChangedIds.size} recordings had file-size changes — cleared cache")
            }

            // 7. Upsert ALL server recordings into the database.
            //    For entries that already exist, we preserve their cached file
            //    paths (unless size changed) and ALWAYS preserve favorite/new flags.
            val entitiesToUpsert = serverRecordings.map { recording ->
                // For any common id, read the old row to carry forward local state.
                val existingEntity = if (recording.id in commonIds) {
                    dao.getById(recording.id)
                } else {
                    null
                }
                val keepPaths = recording.id in commonIds && recording.id !in sizeChangedIds
                recording.toEntity(
                    cachedAudioPath = if (keepPaths) existingEntity?.cachedAudioPath else null,
                    cachedMidiPath = if (keepPaths) existingEntity?.cachedMidiPath else null,
                    isFavorite = existingEntity?.isFavorite ?: false,
                    isNew = existingEntity?.isNew ?: false,
                    favoritedAt = existingEntity?.favoritedAt ?: 0L
                )
            }
            dao.upsert(entitiesToUpsert)

            // 7b. Mark genuinely-new entries with the "new" badge — but NOT on the
            //     very first sync (otherwise every recording would be flagged new).
            if (!isFirstSync && addedIds.isNotEmpty()) {
                dao.markNew(addedIds)
                Log.d(TAG, "Flagged ${addedIds.size} new recordings with badge")
            }

            // 8. Store the new revision
            context.dataStore.edit { it[KEY_REVISION] = serverRevision }

            Log.d(TAG, "Sync complete: +${addedIds.size} -${removedIds.size} ~${sizeChangedIds.size}")

            SyncResult.Updated(
                added = addedIds.size,
                removed = removedIds.size,
                sizeChanged = sizeChangedIds.size
            )

        } catch (e: Exception) {
            Log.e(TAG, "Sync failed", e)
            SyncResult.Failed(e.message ?: e.toString())
        }
    }

    /** Get the stored revision, or -1 if we've never synced. */
    suspend fun getCachedRevision(): Int {
        val prefs = context.dataStore.data.first()
        return prefs[KEY_REVISION] ?: -1
    }

    /** True if we have any cached recordings at all (i.e. not first launch). */
    suspend fun hasCachedData(): Boolean = dao.count() > 0

    // ── File cleanup helpers ───────────────────────────────────────────

    private suspend fun deleteFilesForIds(ids: Set<String>) {
        for (id in ids) {
            deleteFilesForId(id)
        }
    }

    private suspend fun deleteFilesForId(id: String) {
        val entity = dao.getById(id) ?: return
        entity.cachedAudioPath?.let { File(it).delete() }
        entity.cachedMidiPath?.let { File(it).delete() }
    }
}
