package com.radiola.data.repository

import android.content.Context
import android.content.Intent
import android.os.Environment
import android.util.Log
import androidx.core.content.ContextCompat
import com.radiola.data.local.AppDatabase
import com.radiola.data.local.entity.RecordingEntity
import com.radiola.domain.model.Recording
import com.radiola.domain.model.Station
import com.radiola.domain.model.Track
import com.radiola.domain.model.TrackMarker
import com.radiola.domain.repository.NowPlayingRepository
import com.radiola.domain.repository.RecordingRepository
import com.radiola.service.RecordingService
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import javax.inject.Inject
import kotlin.coroutines.coroutineContext

class RecordingRepositoryImpl @Inject constructor(
    private val db: AppDatabase,
    private val okHttpClient: OkHttpClient,
    private val nowPlayingRepository: NowPlayingRepository,
    @ApplicationContext private val context: Context
) : RecordingRepository {

    private val _isRecording = kotlinx.coroutines.flow.MutableStateFlow(false)
    override val isRecording: StateFlow<Boolean> = _isRecording.asStateFlow()

    private var recordingJob: Job? = null
    private var markerJob: Job? = null
    private var currentCall: okhttp3.Call? = null
    private var currentRecordingId: Long? = null

    override fun getRecordings(): Flow<List<Recording>> {
        return db.recordingDao().getAll().map { entities ->
            entities.map { it.toDomain() }
        }
    }

    override suspend fun startRecording(station: Station, track: Track?) {
        if (_isRecording.value) return

        val id = System.currentTimeMillis()
        currentRecordingId = id

        val dir = File(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC), "radiola_recordings")
        dir.mkdirs()

        val isHls = station.streamUrl.contains(".m3u8", ignoreCase = true)
        val ext = when {
            isHls -> "ts"
            station.streamUrl.contains(".aac", ignoreCase = true) -> "aac"
            station.streamUrl.contains(".mp3", ignoreCase = true) -> "mp3"
            else -> "audio"
        }
        val safeName = station.name.replace(Regex("[^a-zA-Z0-9а-яА-ЯёЁ]"), "_").take(30)
        val file = File(dir, "${safeName}_${id}.$ext")

        val entity = RecordingEntity(
            id = id,
            stationName = station.name,
            stationId = station.id,
            filePath = file.absolutePath,
            startTime = id,
            endTime = null,
            trackName = track?.let { "${it.artist} - ${it.song}" },
            duration = null
        )
        db.recordingDao().insert(entity)
        _isRecording.value = true

        // Захват тайм-кодов треков: первый трек на 0, далее по смене now-playing.
        val markers = mutableListOf<TrackMarker>()
        track?.let {
            if (it.artist.isNotBlank() || it.song.isNotBlank()) {
                markers.add(TrackMarker(0L, it.artist, it.song))
            }
        }
        markerJob = CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
            if (markers.isNotEmpty()) {
                try { db.recordingDao().updateMarkers(id, encodeMarkers(markers)) } catch (_: Exception) {}
            }
            // Свой поллинг now-playing — чтобы метки писались независимо от экрана радио
            launch {
                while (isActive) {
                    try { nowPlayingRepository.refreshNowPlaying() } catch (_: Exception) {}
                    delay(8_000) // чаще — точнее тайм-коды треков в записи
                }
            }
            nowPlayingRepository.getNowPlaying(station.id)
                .distinctUntilChangedBy { "${it?.artist}|${it?.song}" }
                .collect { t ->
                    if (t == null || (t.artist.isBlank() && t.song.isBlank())) return@collect
                    val last = markers.lastOrNull()
                    if (last != null && last.artist == t.artist && last.song == t.song) return@collect
                    val offset = (System.currentTimeMillis() - id).coerceAtLeast(0L)
                    markers.add(TrackMarker(offset, t.artist, t.song))
                    try { db.recordingDao().updateMarkers(id, encodeMarkers(markers)) } catch (_: Exception) {}
                }
        }

        // Start foreground service to keep process alive during recording
        val serviceIntent = Intent(context, RecordingService::class.java).apply {
            putExtra(RecordingService.EXTRA_STATION_NAME, station.name)
        }
        ContextCompat.startForegroundService(context, serviceIntent)

        recordingJob = CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
            var output: FileOutputStream? = null
            try {
                output = FileOutputStream(file)
                if (isHls) {
                    recordHls(station.streamUrl, output)
                } else {
                    recordRaw(station.streamUrl, output)
                }
            } catch (e: IOException) {
                if (e.message?.contains("Canceled") == true) {
                    Log.d("RecordingRepo", "Recording cancelled normally")
                } else {
                    Log.e("RecordingRepo", "Recording error", e)
                }
            } finally {
                try { output?.close() } catch (_: Exception) {}
            }
        }
    }

    /** Запись сплошного потока (ICY/Icecast): просто пишем тело ответа в файл. */
    private suspend fun recordRaw(streamUrl: String, output: FileOutputStream) {
        val request = Request.Builder().url(streamUrl).build()
        val call = okHttpClient.newCall(request)
        currentCall = call
        val response = call.execute()
        if (!response.isSuccessful) {
            Log.e("RecordingRepo", "HTTP error: ${response.code}")
            return
        }
        val input = response.body?.byteStream() ?: run {
            Log.e("RecordingRepo", "Empty response body")
            return
        }
        val buffer = ByteArray(8192)
        while (coroutineContext.isActive) {
            val bytesRead = input.read(buffer)
            if (bytesRead == -1) break
            output.write(buffer, 0, bytesRead)
        }
        input.close()
    }

    /**
     * Запись HLS-станций (EMG: Европа Плюс, Ретро FM и др.). Поток — это m3u8-плейлист,
     * а не сплошной поток, поэтому скачиваем .ts-сегменты и склеиваем в файл
     * (валидный MPEG-TS, ExoPlayer его проигрывает и перематывает).
     */
    private suspend fun recordHls(streamUrl: String, output: FileOutputStream) {
        // 1. Резолвим мастер-плейлист в медиа-плейлист (берём первый вариант — у EMG
        //    он наибольшего битрейта).
        var mediaUrl = streamUrl
        val firstText = httpGetText(mediaUrl) ?: return
        if (firstText.contains("#EXT-X-STREAM-INF")) {
            val variant = firstText.lineSequence()
                .map { it.trim() }
                .firstOrNull { it.isNotEmpty() && !it.startsWith("#") }
            if (variant != null) mediaUrl = resolveUrl(mediaUrl, variant)
        }

        // 2. Опрашиваем медиа-плейлист, дописываем новые сегменты.
        val downloaded = LinkedHashSet<String>()
        var firstPass = true
        while (coroutineContext.isActive) {
            val text = httpGetText(mediaUrl)
            if (text == null) { delay(2000); continue }
            var targetDur = 6
            val segments = mutableListOf<String>()
            for (raw in text.lineSequence()) {
                val line = raw.trim()
                when {
                    line.startsWith("#EXT-X-TARGETDURATION:") ->
                        targetDur = line.substringAfter(":").toIntOrNull() ?: targetDur
                    line.isNotEmpty() && !line.startsWith("#") ->
                        segments.add(resolveUrl(mediaUrl, line))
                }
            }
            segments.forEachIndexed { i, segUrl ->
                if (!coroutineContext.isActive || downloaded.contains(segUrl)) return@forEachIndexed
                downloaded.add(segUrl)
                // На первом проходе пропускаем «прошлое» окно (пишем только хвост),
                // чтобы запись начиналась примерно с момента нажатия.
                if (firstPass && i < segments.size - 2) return@forEachIndexed
                httpGetBytes(segUrl)?.let { output.write(it); output.flush() }
            }
            firstPass = false
            if (downloaded.size > 500) {
                val keep = downloaded.toList().takeLast(200)
                downloaded.clear(); downloaded.addAll(keep)
            }
            delay((targetDur * 500L).coerceIn(2000L, 6000L))
        }
    }

    private fun httpGetText(url: String): String? = try {
        okHttpClient.newCall(Request.Builder().url(url).header("User-Agent", "radiOLA").build())
            .execute().use { if (it.isSuccessful) it.body?.string() else null }
    } catch (e: Exception) { Log.w("RecordingRepo", "playlist fetch fail: ${e.message}"); null }

    private fun httpGetBytes(url: String): ByteArray? = try {
        okHttpClient.newCall(Request.Builder().url(url).header("User-Agent", "radiOLA").build())
            .execute().use { if (it.isSuccessful) it.body?.bytes() else null }
    } catch (e: Exception) { Log.w("RecordingRepo", "segment fetch fail: ${e.message}"); null }

    /** Разрешает относительный URL (вариант/сегмент) относительно базового плейлиста. */
    private fun resolveUrl(base: String, ref: String): String =
        try { java.net.URI(base).resolve(ref).toString() } catch (e: Exception) { ref }

    override suspend fun stopRecording() {
        currentCall?.cancel()
        currentCall = null
        recordingJob?.cancelAndJoin()
        recordingJob = null
        markerJob?.cancel()
        markerJob = null
        _isRecording.value = false

        // Stop foreground service
        context.stopService(Intent(context, RecordingService::class.java))

        currentRecordingId?.let { id ->
            val endTime = System.currentTimeMillis()
            val duration = endTime - id
            try {
                db.recordingDao().updateEndTime(id, endTime, duration)
            } catch (e: Exception) {
                Log.e("RecordingRepo", "Failed to update recording end time", e)
            }
        }
        currentRecordingId = null
    }

    override suspend fun deleteRecording(id: Long) {
        val entity = db.recordingDao().getById(id)
        entity?.let {
            try { File(it.filePath).delete() } catch (_: Exception) {}
        }
        db.recordingDao().deleteById(id)
    }

    private fun RecordingEntity.toDomain(): Recording = Recording(
        id = id,
        stationName = stationName,
        stationId = stationId,
        filePath = filePath,
        startTime = startTime,
        endTime = endTime,
        trackName = trackName,
        duration = duration,
        markers = decodeMarkers(markers)
    )

    // Метки кодируем строкой "offsetMs\tartist\tsong" по строкам \n
    // (названия треков не содержат \t/\n).
    private fun encodeMarkers(list: List<TrackMarker>): String =
        list.joinToString("\n") { "${it.offsetMs}\t${it.artist}\t${it.song}" }

    private fun decodeMarkers(raw: String): List<TrackMarker> {
        if (raw.isBlank()) return emptyList()
        return raw.split("\n").mapNotNull { line ->
            val p = line.split("\t")
            if (p.size != 3) return@mapNotNull null
            val off = p[0].toLongOrNull() ?: return@mapNotNull null
            TrackMarker(off, p[1], p[2])
        }
    }
}
