package com.hipograf.companion.health

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.BasalMetabolicRateRecord
import androidx.health.connect.client.records.BloodGlucoseRecord
import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.BodyFatRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.OxygenSaturationRecord
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.records.WeightRecord
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter

/**
 * Reads health data from Android Health Connect.
 *
 * Health Connect aggregates data from Samsung Health, Google Fit, Fitbit (when connected),
 * and other health apps installed on the device. The user must have:
 *  1. Health Connect installed (built into Android 14+, otherwise from Play Store)
 *  2. Samsung Health configured to sync to Health Connect:
 *     Samsung Health → Settings → Health Connect → enable
 *  3. Granted this app the necessary read permissions
 */
object HealthReader {

    private const val TAG = "HipografHealth"
    private val DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE

    class MissingPermissionsException : Exception("Health Connect permissions not granted")

    val PERMISSIONS = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getReadPermission(SleepSessionRecord::class),
        HealthPermission.getReadPermission(OxygenSaturationRecord::class),
        HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
        HealthPermission.getReadPermission(NutritionRecord::class),
        HealthPermission.getReadPermission(WeightRecord::class),
        HealthPermission.getReadPermission(BodyFatRecord::class),
        HealthPermission.getReadPermission(BasalMetabolicRateRecord::class),
        HealthPermission.getReadPermission(BloodGlucoseRecord::class),
        HealthPermission.getReadPermission(BloodPressureRecord::class),
    )

    data class HealthReading(
        val metric: String,
        val value: Double,
        val unit: String,
        val date: String,
        val metricType: String = "daily_summary",
        val quality: String = "good"
    )

    /**
     * Check whether Health Connect is available on this device.
     */
    fun availability(context: Context): Int {
        return HealthConnectClient.getSdkStatus(context)
    }

    fun isAvailable(context: Context): Boolean {
        return availability(context) == HealthConnectClient.SDK_AVAILABLE
    }

    suspend fun hasAllPermissions(context: Context): Boolean {
        if (!isAvailable(context)) return false
        val client = HealthConnectClient.getOrCreate(context)
        val granted = client.permissionController.getGrantedPermissions()
        return granted.containsAll(PERMISSIONS)
    }

    /**
     * Read all available daily summary readings since the given timestamp.
     * If sinceTimestamp == 0L, reads the last 30 days.
     */
    suspend fun readSince(context: Context, sinceTimestamp: Long): List<HealthReading> {
        if (!isAvailable(context)) {
            Log.w(TAG, "Health Connect not available")
            return emptyList()
        }

        val client = HealthConnectClient.getOrCreate(context)
        val granted = client.permissionController.getGrantedPermissions()
        Log.i(TAG, "Granted permissions: ${granted.size}/${PERMISSIONS.size}")
        if (!granted.containsAll(PERMISSIONS)) {
            val missing = PERMISSIONS - granted
            Log.w(TAG, "Missing permissions: $missing")
            throw MissingPermissionsException()
        }

        val zone = ZoneId.systemDefault()
        val endInstant = Instant.now()
        val startInstant = if (sinceTimestamp > 0L) {
            Instant.ofEpochMilli(sinceTimestamp)
        } else {
            endInstant.minusSeconds(30 * 24 * 60 * 60L) // last 30 days
        }
        Log.i(TAG, "Reading from $startInstant to $endInstant")

        val startDate = startInstant.atZone(zone).toLocalDate()
        val endDate = endInstant.atZone(zone).toLocalDate()

        val readings = mutableListOf<HealthReading>()

        // Iterate each day in range and aggregate per-day totals
        var day = startDate
        while (!day.isAfter(endDate)) {
            val dayStart = day.atStartOfDay(zone).toInstant()
            val dayEnd = day.plusDays(1).atStartOfDay(zone).toInstant()
            val range = TimeRangeFilter.between(dayStart, dayEnd)
            val dateStr = day.format(DATE_FMT)

            // Steps — use aggregate() so Health Connect deduplicates across data sources
            // (Samsung Health, Google Fit, phone step counter all writing the same period)
            try {
                val stepsAgg = client.aggregate(
                    AggregateRequest(
                        metrics = setOf(StepsRecord.COUNT_TOTAL),
                        timeRangeFilter = range
                    )
                )
                val totalSteps = stepsAgg[StepsRecord.COUNT_TOTAL] ?: 0L
                if (totalSteps > 0) {
                    readings.add(HealthReading("steps", totalSteps.toDouble(), "steps", dateStr))
                }
            } catch (e: Exception) { Log.w(TAG, "Steps read failed for $dateStr: ${e.message}") }

            // Total calories — aggregate to dedupe across sources.
            // IMPORTANT: TotalCaloriesBurned aggregate falls back to a BMR estimate
            // (computed from height/weight/age) if there are no real records — that
            // estimate has nothing to do with the user's actual activity, so we
            // explicitly check that at least one real record exists before emitting.
            try {
                val calRecs = client.readRecords(
                    ReadRecordsRequest(TotalCaloriesBurnedRecord::class, timeRangeFilter = range)
                )
                if (calRecs.records.isNotEmpty()) {
                    val calAgg = client.aggregate(
                        AggregateRequest(
                            metrics = setOf(TotalCaloriesBurnedRecord.ENERGY_TOTAL),
                            timeRangeFilter = range
                        )
                    )
                    val totalKcal = calAgg[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0
                    if (totalKcal > 0) {
                        readings.add(HealthReading("calories_total", totalKcal, "kcal", dateStr))
                    }
                }
            } catch (e: Exception) { Log.w(TAG, "Calories total read failed for $dateStr: ${e.message}") }

            // Food calories — what the user actually ate (Samsung Health "Food" log).
            // NutritionRecord.energy is the energy value the user (or app) recorded
            // for a meal. Aggregate sums every meal logged that day.
            try {
                val foodAgg = client.aggregate(
                    AggregateRequest(
                        metrics = setOf(NutritionRecord.ENERGY_TOTAL),
                        timeRangeFilter = range
                    )
                )
                val foodKcal = foodAgg[NutritionRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0
                if (foodKcal > 0) {
                    readings.add(HealthReading("calories_intake", foodKcal, "kcal", dateStr))
                }
            } catch (e: Exception) { Log.w(TAG, "Food calories read failed for $dateStr: ${e.message}") }

            // Weight — latest measurement of the day if any
            try {
                val weightResp = client.readRecords(
                    ReadRecordsRequest(WeightRecord::class, timeRangeFilter = range)
                )
                val latestWeight = weightResp.records.maxByOrNull { it.time }
                if (latestWeight != null) {
                    readings.add(HealthReading(
                        "weight", latestWeight.weight.inKilograms, "kg", dateStr
                    ))
                }
            } catch (e: Exception) { Log.w(TAG, "Weight read failed for $dateStr: ${e.message}") }

            // Body fat percentage (latest of the day)
            try {
                val bfResp = client.readRecords(
                    ReadRecordsRequest(BodyFatRecord::class, timeRangeFilter = range)
                )
                val latestBf = bfResp.records.maxByOrNull { it.time }
                if (latestBf != null) {
                    readings.add(HealthReading(
                        "body_fat", latestBf.percentage.value, "percent", dateStr
                    ))
                }
            } catch (e: Exception) { Log.w(TAG, "Body fat read failed for $dateStr: ${e.message}") }

            // Basal metabolic rate (latest of the day, converted to kcal/day)
            try {
                val bmrResp = client.readRecords(
                    ReadRecordsRequest(BasalMetabolicRateRecord::class, timeRangeFilter = range)
                )
                val latestBmr = bmrResp.records.maxByOrNull { it.time }
                if (latestBmr != null) {
                    // basalMetabolicRate is a Power; multiply by 24h to get kcal/day
                    val kcalPerDay = latestBmr.basalMetabolicRate.inKilocaloriesPerDay
                    readings.add(HealthReading(
                        "basal_metabolic_rate", kcalPerDay, "kcal", dateStr
                    ))
                }
            } catch (e: Exception) { Log.w(TAG, "BMR read failed for $dateStr: ${e.message}") }

            // Blood glucose (latest of the day, converted to mg/dL)
            try {
                val bgResp = client.readRecords(
                    ReadRecordsRequest(BloodGlucoseRecord::class, timeRangeFilter = range)
                )
                val latestBg = bgResp.records.maxByOrNull { it.time }
                if (latestBg != null) {
                    readings.add(HealthReading(
                        "blood_glucose",
                        latestBg.level.inMilligramsPerDeciliter,
                        "mg/dL", dateStr
                    ))
                }
            } catch (e: Exception) { Log.w(TAG, "Blood glucose read failed for $dateStr: ${e.message}") }

            // Blood pressure — latest measurement of the day if any
            try {
                val bpResp = client.readRecords(
                    ReadRecordsRequest(BloodPressureRecord::class, timeRangeFilter = range)
                )
                val latestBp = bpResp.records.maxByOrNull { it.time }
                if (latestBp != null) {
                    readings.add(HealthReading(
                        "blood_pressure_sys",
                        latestBp.systolic.inMillimetersOfMercury,
                        "mmHg", dateStr
                    ))
                    readings.add(HealthReading(
                        "blood_pressure_dia",
                        latestBp.diastolic.inMillimetersOfMercury,
                        "mmHg", dateStr
                    ))
                }
            } catch (e: Exception) { Log.w(TAG, "Blood pressure read failed for $dateStr: ${e.message}") }

            // Heart rate — Samsung Health doesn't write Resting HR to Health Connect,
            // only HR samples. We compute the average of all samples for the day and
            // surface it under heart_rate_resting (the cleanest single number we can
            // give the clinical UI).
            try {
                val hrResp = client.readRecords(
                    ReadRecordsRequest(HeartRateRecord::class, timeRangeFilter = range)
                )
                val allBpm = hrResp.records.flatMap { rec -> rec.samples.map { it.beatsPerMinute } }
                if (allBpm.isNotEmpty()) {
                    val avg = allBpm.average()
                    readings.add(HealthReading("heart_rate_resting", avg, "bpm", dateStr))
                }
            } catch (e: Exception) { Log.w(TAG, "Heart rate read failed for $dateStr: ${e.message}") }

            // SpO2
            try {
                val spo2Resp = client.readRecords(
                    ReadRecordsRequest(OxygenSaturationRecord::class, timeRangeFilter = range)
                )
                if (spo2Resp.records.isNotEmpty()) {
                    val avgSpo2 = spo2Resp.records.map { it.percentage.value }.average()
                    readings.add(HealthReading("blood_oxygen", avgSpo2, "percent", dateStr))
                }
            } catch (e: Exception) { Log.w(TAG, "SpO2 read failed for $dateStr: ${e.message}") }

            day = day.plusDays(1)
        }

        // Sleep — read once across the full range, then attribute each session
        // to its WAKE-UP DAY (the local date of session.endTime). This matches
        // Samsung Health's convention: a sleep that goes 23:00 → 07:00 is shown
        // on the day you woke up, not the day you went to bed.
        try {
            // Pad the start by 1 day so we catch sleep sessions whose wake-up
            // day falls within our requested range but whose start is the prior day.
            val sleepRange = TimeRangeFilter.between(
                startInstant.minusSeconds(24 * 60 * 60L),
                endInstant
            )
            val sleepResp = client.readRecords(
                ReadRecordsRequest(SleepSessionRecord::class, timeRangeFilter = sleepRange)
            )
            Log.i(TAG, "Sleep sessions found: ${sleepResp.records.size}")

            // Group by wake-up local date, sum durations
            val durationByDate = mutableMapOf<LocalDate, Double>()
            for (session in sleepResp.records) {
                val wakeDate = session.endTime.atZone(zone).toLocalDate()
                if (wakeDate.isBefore(startDate) || wakeDate.isAfter(endDate)) continue
                val hours = (session.endTime.toEpochMilli() - session.startTime.toEpochMilli()) / (1000.0 * 60 * 60)
                durationByDate.merge(wakeDate, hours) { a, b -> a + b }
            }
            for ((date, hours) in durationByDate) {
                readings.add(HealthReading("sleep_duration", hours, "hours", date.format(DATE_FMT)))
            }
        } catch (e: Exception) { Log.w(TAG, "Sleep read failed: ${e.message}") }

        Log.i(TAG, "Total readings collected: ${readings.size}")
        val byMetric = readings.groupBy { it.metric }.mapValues { it.value.size }
        Log.i(TAG, "By metric: $byMetric")
        return readings
    }

    fun getDeviceInfo(): Map<String, String> {
        return mapOf(
            "model" to Build.MODEL,
            "manufacturer" to Build.MANUFACTURER,
            "osVersion" to "Android ${Build.VERSION.RELEASE}",
            "sdkInt" to Build.VERSION.SDK_INT.toString(),
            "source" to "health_connect"
        )
    }

    /**
     * Probe Health Connect for what's actually stored on the device.
     *
     * For each record type we care about, this reports:
     *  - whether read permission is granted
     *  - how many records exist in the last 7 days
     *  - which package(s) wrote them (so we can see if Samsung Health is the source)
     *  - today's aggregate value (so the user can compare directly with the
     *    Samsung Health UI and tell us "this is wrong")
     *
     * Returns a single human-readable string we can dump into an AlertDialog.
     */
    suspend fun diagnose(context: Context): String {
        val sb = StringBuilder()

        val sdkStatus = HealthConnectClient.getSdkStatus(context)
        sb.appendLine("=== Health Connect ===")
        sb.appendLine("SDK status: $sdkStatus (3 = available)")
        if (sdkStatus != HealthConnectClient.SDK_AVAILABLE) {
            sb.appendLine("Health Connect not available — cannot diagnose.")
            return sb.toString()
        }

        val client = HealthConnectClient.getOrCreate(context)
        val granted = client.permissionController.getGrantedPermissions()
        sb.appendLine("Granted: ${granted.size}/${PERMISSIONS.size} permissions")
        val missing = PERMISSIONS - granted
        if (missing.isNotEmpty()) {
            sb.appendLine("MISSING:")
            for (p in missing) sb.appendLine("  - $p")
        }
        sb.appendLine()

        val zone = ZoneId.systemDefault()
        val today = LocalDate.now(zone)
        val todayStart = today.atStartOfDay(zone).toInstant()
        val todayEnd = today.plusDays(1).atStartOfDay(zone).toInstant()
        val weekStart = today.minusDays(6).atStartOfDay(zone).toInstant()
        val weekRange = TimeRangeFilter.between(weekStart, todayEnd)
        val todayRange = TimeRangeFilter.between(todayStart, todayEnd)

        sb.appendLine("=== Today: ${today.format(DATE_FMT)} ===")
        sb.appendLine("(Compare these to what Samsung Health shows you right now)")
        sb.appendLine()

        // Steps — also break down per-day for the last 7 days so we can see
        // exactly which calendar days have data (not just a record count).
        probeRecord(sb, "Steps", granted, HealthPermission.getReadPermission(StepsRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(StepsRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val perDay = StringBuilder()
            for (offset in 6 downTo 0) {
                val d = today.minusDays(offset.toLong())
                val r = TimeRangeFilter.between(
                    d.atStartOfDay(zone).toInstant(),
                    d.plusDays(1).atStartOfDay(zone).toInstant()
                )
                val v = client.aggregate(AggregateRequest(setOf(StepsRecord.COUNT_TOTAL), r))[StepsRecord.COUNT_TOTAL] ?: 0L
                if (v > 0) perDay.append("\n    ${d.format(DATE_FMT)}: $v")
            }
            val daysWithData = perDay.toString().ifEmpty { "\n    (no days with non-zero steps)" }
            "${recs.size} records (7d) | sources: $sources$daysWithData"
        }

        // Total calories
        probeRecord(sb, "TotalCaloriesBurned", granted, HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(TotalCaloriesBurnedRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val agg = client.aggregate(AggregateRequest(setOf(TotalCaloriesBurnedRecord.ENERGY_TOTAL), todayRange))
            val todayVal = agg[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0
            "${"%.0f".format(todayVal)} kcal today | ${recs.size} records (7d) | sources: $sources"
        }

        // Nutrition (food intake)
        probeRecord(sb, "Nutrition (food intake)", granted, HealthPermission.getReadPermission(NutritionRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(NutritionRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val agg = client.aggregate(AggregateRequest(setOf(NutritionRecord.ENERGY_TOTAL), todayRange))
            val todayVal = agg[NutritionRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0
            "${"%.0f".format(todayVal)} kcal today | ${recs.size} meal records (7d) | sources: $sources"
        }

        // Heart rate samples (Samsung doesn't write Resting HR — we average samples)
        probeRecord(sb, "HeartRate (samples)", granted, HealthPermission.getReadPermission(HeartRateRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(HeartRateRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val sampleCount = recs.sumOf { it.samples.size }
            "${recs.size} record blocks, $sampleCount total samples (7d) | sources: $sources"
        }

        // SpO2
        probeRecord(sb, "OxygenSaturation (SpO2)", granted, HealthPermission.getReadPermission(OxygenSaturationRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(OxygenSaturationRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            "${recs.size} records (7d) | sources: $sources"
        }

        // Sleep — list each session with its bedtime → wake time and the
        // wake-up date we'd attribute it to. This makes it obvious whether
        // a multi-hour night was written as one session or split.
        probeRecord(sb, "SleepSession", granted, HealthPermission.getReadPermission(SleepSessionRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(SleepSessionRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val sb2 = StringBuilder()
            for (s in recs.sortedBy { it.startTime }) {
                val start = s.startTime.atZone(zone)
                val end = s.endTime.atZone(zone)
                val hours = (s.endTime.toEpochMilli() - s.startTime.toEpochMilli()) / (1000.0 * 60 * 60)
                sb2.append("\n    ")
                    .append(start.toLocalDate().format(DATE_FMT)).append(" ").append("%02d:%02d".format(start.hour, start.minute))
                    .append(" → ")
                    .append(end.toLocalDate().format(DATE_FMT)).append(" ").append("%02d:%02d".format(end.hour, end.minute))
                    .append(" = ").append("%.2fh".format(hours))
                    .append(" (wake-day ").append(end.toLocalDate().format(DATE_FMT)).append(")")
            }
            "${recs.size} sessions (7d) | sources: $sources$sb2"
        }

        // Weight
        probeRecord(sb, "Weight", granted, HealthPermission.getReadPermission(WeightRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(WeightRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val latest = recs.maxByOrNull { it.time }?.weight?.inKilograms
            "${latest?.let { "%.1f".format(it) } ?: "-"} kg latest | ${recs.size} records (7d) | sources: $sources"
        }

        // Body fat
        probeRecord(sb, "BodyFat", granted, HealthPermission.getReadPermission(BodyFatRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(BodyFatRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val latest = recs.maxByOrNull { it.time }?.percentage?.value
            "${latest?.let { "%.1f".format(it) } ?: "-"} %% latest | ${recs.size} records (7d) | sources: $sources"
        }

        // BMR (basal metabolic rate)
        probeRecord(sb, "BasalMetabolicRate", granted, HealthPermission.getReadPermission(BasalMetabolicRateRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(BasalMetabolicRateRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val latest = recs.maxByOrNull { it.time }?.basalMetabolicRate?.inKilocaloriesPerDay
            "${latest?.let { "%.0f".format(it) } ?: "-"} kcal/day latest | ${recs.size} records (7d) | sources: $sources"
        }

        // Blood glucose
        probeRecord(sb, "BloodGlucose", granted, HealthPermission.getReadPermission(BloodGlucoseRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(BloodGlucoseRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val latest = recs.maxByOrNull { it.time }?.level?.inMilligramsPerDeciliter
            "${latest?.let { "%.0f".format(it) } ?: "-"} mg/dL latest | ${recs.size} records (7d) | sources: $sources"
        }

        // Blood pressure
        probeRecord(sb, "BloodPressure", granted, HealthPermission.getReadPermission(BloodPressureRecord::class)) {
            val recs = client.readRecords(ReadRecordsRequest(BloodPressureRecord::class, timeRangeFilter = weekRange)).records
            val sources = recs.map { it.metadata.dataOrigin.packageName }.toSet()
            val latest = recs.maxByOrNull { it.time }
            val bp = if (latest != null) {
                "${"%.0f".format(latest.systolic.inMillimetersOfMercury)}/${"%.0f".format(latest.diastolic.inMillimetersOfMercury)}"
            } else "-"
            "$bp mmHg latest | ${recs.size} records (7d) | sources: $sources"
        }

        sb.appendLine()
        sb.appendLine("=== Tip ===")
        sb.appendLine("If a category shows 0 records, open Samsung Health → Settings → Health Connect → Manage data and make sure that category is enabled for sharing.")

        return sb.toString()
    }

    private suspend fun probeRecord(
        sb: StringBuilder,
        label: String,
        granted: Set<String>,
        permission: String,
        block: suspend () -> String
    ) {
        sb.append("• $label: ")
        if (permission !in granted) {
            sb.appendLine("PERMISSION DENIED")
            return
        }
        try {
            sb.appendLine(block())
        } catch (e: Exception) {
            sb.appendLine("ERROR — ${e.message}")
        }
    }
}
