/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.ui.player

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.runicgateway.app.R
import com.runicgateway.app.data.api.dto.CharPointsDto
import com.runicgateway.app.data.api.dto.CharProfileDto
import com.runicgateway.app.data.api.dto.CharStatsDto
import com.runicgateway.app.data.api.dto.EquipmentDto
import com.runicgateway.app.data.api.dto.ResistDto
import com.runicgateway.app.data.api.dto.SkillDto
import com.runicgateway.app.data.api.dto.TitlesDto
import com.runicgateway.app.ui.UiState
import com.runicgateway.app.ui.components.ErrorView
import com.runicgateway.app.ui.components.LoadingView
import com.runicgateway.app.ui.components.SectionLabel
import com.runicgateway.app.ui.components.ShardCard
import com.runicgateway.app.ui.components.StatBar
import kotlinx.serialization.json.jsonPrimitive

/**
 * A text-only character sheet (PLAN.md §6.3): identity + standing, attributes and
 * vitals, resistances, skills, and equipment. No item icons / paperdoll art — a
 * richer view is a future enhancement pending the platform art work.
 */
@Composable
fun CharacterSheetScreen(
    modifier: Modifier = Modifier,
    viewModel: CharacterViewModel = hiltViewModel(),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    when (val s = state) {
        is UiState.Loading -> LoadingView(modifier)
        is UiState.Error -> ErrorView(s.kind, onRetry = viewModel::load, modifier = modifier)
        is UiState.Success -> CharacterSheet(s.data, modifier)
    }
}

@Composable
private fun CharacterSheet(char: CharProfileDto, modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
    ) {
        IdentityBlock(char)
        StandingChips(char)
        char.stats?.let { AttributesBlock(it) }
        char.stats?.resist?.let { ResistancesBlock(it) }
        SkillsBlock(char.skills)
        PointsBlock(displayPoints(char))
        EquipmentBlock(char.equipment)
    }
}

@Composable
private fun IdentityBlock(char: CharProfileDto) {
    Column {
        Text(
            char.name ?: stringResource(R.string.player_char_unknown),
            style = MaterialTheme.typography.headlineSmall,
        )
        char.title?.takeIf { it.isNotBlank() }?.let {
            Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
        }
        Row(Modifier.padding(top = 4.dp)) {
            Text(
                stringResource(if (char.online) R.string.player_char_online else R.string.player_char_offline),
                style = MaterialTheme.typography.labelMedium,
                color = if (char.online) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
            )
            char.serial?.let {
                Text(
                    "  ·  $it",
                    style = MaterialTheme.typography.labelMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                )
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun StandingChips(char: CharProfileDto) {
    val chips = buildList {
        char.governorOf.forEach { add(stringResource(R.string.player_char_governor, it)) }
        char.guild?.let { g ->
            val abbr = g.abbr?.let { " [$it]" } ?: ""
            add(stringResource(R.string.player_char_guildmaster, "${g.name.orEmpty()}$abbr"))
        }
        addAll(displayTitles(char.titles))
    }
    if (chips.isEmpty()) return
    FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
        chips.forEach { Chip(it) }
    }
}

@Composable
private fun Chip(text: String) {
    Surface(
        color = MaterialTheme.colorScheme.secondaryContainer,
        shape = MaterialTheme.shapes.small,
    ) {
        Text(
            text,
            style = MaterialTheme.typography.labelMedium,
            color = MaterialTheme.colorScheme.onSecondaryContainer,
            modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
        )
    }
}

@Composable
private fun AttributesBlock(stats: CharStatsDto) {
    SheetCard(R.string.player_char_attributes) {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Stat(stringResource(R.string.player_char_str), stats.str)
            Stat(stringResource(R.string.player_char_dex), stats.dex)
            Stat(stringResource(R.string.player_char_int), stats.int)
        }
        Column(Modifier.padding(top = 16.dp)) {
            SectionLabel(stringResource(R.string.player_char_vitals))
            Column(Modifier.padding(top = 8.dp)) {
                Vital(stringResource(R.string.player_char_hits), stats.hits, stats.hitsMax)
                Vital(stringResource(R.string.player_char_mana), stats.mana, stats.manaMax)
                Vital(stringResource(R.string.player_char_stam), stats.stam, stats.stamMax)
            }
        }
    }
}

@Composable
private fun Stat(label: String, value: Int?) {
    Column {
        Text("${value ?: "—"}", style = MaterialTheme.typography.titleLarge)
        Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
    }
}

@Composable
private fun Vital(label: String, cur: Int?, max: Int?) {
    Column(Modifier.padding(vertical = 5.dp)) {
        Row(Modifier.fillMaxWidth().padding(bottom = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
            Text(label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
            Text("${cur ?: "—"} / ${max ?: "—"}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
        }
        val fraction = if (max != null && max > 0 && cur != null) cur.toFloat() / max else 0f
        StatBar(fraction)
    }
}

@Composable
private fun ResistancesBlock(resist: ResistDto) {
    val rows = listOf(
        stringResource(R.string.player_char_phys) to resist.phys,
        stringResource(R.string.player_char_fire) to resist.fire,
        stringResource(R.string.player_char_cold) to resist.cold,
        stringResource(R.string.player_char_pois) to resist.pois,
        stringResource(R.string.player_char_energy) to resist.energy,
    )
    if (rows.all { it.second == null }) return
    SheetCard(R.string.player_char_resistances) {
        Row(Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
            rows.forEach { (label, value) ->
                Column {
                    Text("${value ?: 0}", style = MaterialTheme.typography.titleMedium)
                    Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
                }
            }
        }
    }
}

@Composable
private fun SkillsBlock(skills: List<SkillDto>) {
    val shown = skills
        .filter { (it.value ?: it.base ?: 0.0) > 0.0 }
        .sortedByDescending { it.value ?: 0.0 }
    if (shown.isEmpty()) return
    SheetCard(R.string.player_char_skills) {
        shown.forEach { skill ->
            val value = skill.value ?: 0.0
            Column(Modifier.padding(vertical = 5.dp)) {
                Row(Modifier.fillMaxWidth().padding(bottom = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text(skill.n ?: "—", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
                    Text(formatSkill(value), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Medium)
                }
                StatBar((value / SKILL_CAP).toFloat())
            }
        }
    }
}

/**
 * Loyalty & points standings (Protocol 3.0 §7.3). Renders nothing at all for a
 * character that has earned nothing anywhere, which is a normal state.
 *
 * Only a system with a real cap gets a meter: an uncapped score
 * ([CharPointsDto.maxPoints] `0`, the common case on a real shard) has nothing to be
 * a fraction of, and a full-width bar would imply a completion that doesn't exist.
 */
@Composable
private fun PointsBlock(points: List<CharPointsDto>) {
    if (points.isEmpty()) return
    SheetCard(R.string.player_char_points) {
        points.forEach { entry ->
            val cap = entry.cap
            val score = entry.points ?: 0L
            Column(Modifier.padding(vertical = 5.dp)) {
                Row(
                    Modifier.fillMaxWidth().padding(bottom = 4.dp),
                    horizontalArrangement = Arrangement.SpaceBetween,
                ) {
                    Text(
                        // `rank` is absent unless the shard opts in; absent and
                        // "unranked" are different, so the suffix only appears when sent.
                        entry.rank?.let { stringResource(R.string.player_char_points_ranked, pointsLabel(entry), it) }
                            ?: pointsLabel(entry),
                        style = MaterialTheme.typography.labelMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                    Text(
                        cap?.let { stringResource(R.string.player_char_points_of, score, it) } ?: score.toString(),
                        style = MaterialTheme.typography.labelMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        fontWeight = FontWeight.Medium,
                    )
                }
                if (cap != null) StatBar((score.toDouble() / cap).coerceIn(0.0, 1.0).toFloat())
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun EquipmentBlock(equipment: List<EquipmentDto>) {
    if (equipment.isEmpty()) return
    SheetCard(R.string.player_char_equipment) {
        equipment.forEach { item ->
            Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
                Text(
                    item.label ?: stringResource(R.string.player_char_item),
                    style = MaterialTheme.typography.bodyLarge,
                )
                val meta = listOfNotNull(
                    item.itemId?.let { stringResource(R.string.player_char_item_id, it) },
                    item.hue?.takeIf { it != 0 }?.let { stringResource(R.string.player_char_item_hue, it) },
                ).joinToString("  ·  ")
                if (meta.isNotBlank()) {
                    Text(meta, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
                }
                val mods = item.mods.orEmpty()
                if (mods.isNotEmpty()) {
                    FlowRow(Modifier.padding(top = 4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                        mods.forEach { (k, v) -> Chip("$k ${jsonText(v)}") }
                    }
                }
            }
        }
    }
}

@Composable
private fun SheetCard(titleRes: Int, content: @Composable () -> Unit) {
    ShardCard(Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp)) {
            Text(stringResource(titleRes), style = MaterialTheme.typography.titleMedium)
            content()
        }
    }
}

// ── Pure helpers (unit-tested) ───────────────────────────────────────────────

/** ServUO's default skill cap; used to scale the skill meter fill (0..1). */
private const val SKILL_CAP = 120.0

/** Format a skill value: drop the ".0" on whole numbers, else one decimal. */
internal fun formatSkill(value: Double): String =
    if (value % 1.0 == 0.0) value.toInt().toString() else "%.1f".format(value)

/**
 * The human-readable title chips for a [TitlesDto] (parity with the website's
 * `CharacterSheet.jsx#displayTitles`): fame/karma, skill title, and the selected
 * reward title.
 *
 * Reward entries arrive as either a literal or a cliloc number in string form. The
 * server now resolves the numeric ones into `rewardResolved`, a **parallel** array
 * (see `docs/website/CLILOCS.md`), so the mapping below is index-preserving: an entry
 * that didn't resolve becomes null and is skipped, but must not shift the `selected`
 * index onto its neighbour. A number with no resolution is still skipped rather than
 * rendered as a raw id, which is also the whole behavior on a shard that configures
 * no cliloc table.
 *
 * Falling back to the first title that resolved (rather than showing nothing) matters
 * when the *selected* one is the unresolved entry. De-duplicated, blanks dropped.
 */
internal fun displayTitles(titles: TitlesDto?): List<String> {
    if (titles == null) return emptyList()
    val out = mutableListOf<String>()
    titles.fameKarma?.let { out.add(it) }
    titles.skill?.let { out.add(it) }
    val reward = titles.reward.mapIndexed { i, raw ->
        titles.rewardResolved.getOrNull(i)
            ?: raw.takeUnless { it.isBlank() || it.all(Char::isDigit) }
    }
    val candidate = reward.getOrNull(titles.selected ?: -1) ?: reward.firstNotNullOfOrNull { it }
    if (!candidate.isNullOrBlank()) out.add(candidate)
    return out.filter { it.isNotBlank() }.distinct()
}

/**
 * A point system's display name: the shard's own [CharPointsDto.nameString] when it
 * has one, else the humanised `PointsType` key.
 *
 * The fallback is the PRIMARY path, not a defensive nicety — most systems name
 * themselves with a cliloc, so `nameString` comes back null for four of five boards
 * on a real shard (`docs/link/v3.md` §7.5). Parity with the website's
 * `humanisePoints`.
 */
internal fun pointsLabel(entry: CharPointsDto): String {
    entry.nameString?.takeIf { it.isNotBlank() }?.let { return it }
    val key = entry.system.orEmpty()
    return key
        .replace(Regex("([a-z0-9])([A-Z])"), "$1 $2")
        .replaceFirstChar { it.uppercaseChar() }
}

/**
 * The points block, best standing first, dropping systems the character has no score
 * in. Guarded for an older shard plugin that sends no `points` block at all.
 */
internal fun displayPoints(char: CharProfileDto): List<CharPointsDto> =
    char.points
        .filter { (it.points ?: 0L) > 0L }
        .sortedByDescending { it.points ?: 0L }

private fun jsonText(element: kotlinx.serialization.json.JsonElement): String =
    runCatching { element.jsonPrimitive.content }.getOrElse { element.toString() }
