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

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
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.text.style.TextOverflow
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.BrandDto
import com.runicgateway.app.data.api.dto.PointsBoardDto
import com.runicgateway.app.data.api.dto.PointsEntryDto
import com.runicgateway.app.ui.components.SectionLabel
import com.runicgateway.app.ui.components.ShardCard

/**
 * The points/loyalty leaderboards (PLAN.md §9 M11), one card per system, live via
 * `points.board` frames.
 */
@Composable
fun LeaderboardsScreen(
    brand: BrandDto? = null,
    modifier: Modifier = Modifier,
    viewModel: LeaderboardsViewModel = hiltViewModel(),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val connected by viewModel.connected.collectAsStateWithLifecycle()

    LiveBoardScreen(
        emptyMessage = stringResource(R.string.leaderboards_empty),
        state = state,
        connected = connected,
        onRetry = viewModel::load,
        key = { it.system.orEmpty() },
        modifier = modifier,
    ) { board -> BoardCard(board, placeholderName(brand)) }
}

@Composable
private fun BoardCard(board: PointsBoardDto, placeholderName: String) {
    ShardCard(Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                Text(
                    text = boardLabel(board),
                    style = MaterialTheme.typography.titleMedium,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.weight(1f),
                )
                board.players?.let {
                    Text(
                        text = stringResource(R.string.leaderboards_players, it),
                        style = MaterialTheme.typography.labelSmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
            }
            // A cap is worth stating only when there is one; most systems on a real
            // shard are uncapped (maxPoints 0), and "/ 0" would be nonsense.
            board.cap?.let {
                SectionLabel(stringResource(R.string.leaderboards_cap, it))
            }

            if (board.top.isEmpty()) {
                // A board nobody has scored on still gets a row, so the page reads as a
                // set of standings waiting to be filled rather than a stack of blanks.
                // It is deliberately NOT shaped like an entry — no rank, no score, the
                // instance's own name — because a placeholder that looked like a real
                // standing would be a fabricated one. The first real entry replaces it.
                HorizontalDivider(Modifier.padding(vertical = 8.dp))
                Row(
                    Modifier.fillMaxWidth().padding(vertical = 3.dp),
                    horizontalArrangement = Arrangement.SpaceBetween,
                ) {
                    Text(
                        text = placeholderName,
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis,
                        modifier = Modifier.weight(1f),
                    )
                    Text(
                        text = stringResource(R.string.leaderboards_no_score),
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
                Text(
                    stringResource(R.string.leaderboards_board_empty),
                    style = MaterialTheme.typography.labelSmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    modifier = Modifier.padding(top = 6.dp),
                )
            } else {
                HorizontalDivider(Modifier.padding(vertical = 8.dp))
                board.top.forEach { entry -> EntryRow(entry) }
            }
        }
    }
}

@Composable
private fun EntryRow(entry: PointsEntryDto) {
    Row(
        Modifier.fillMaxWidth().padding(vertical = 3.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(
            text = stringResource(
                R.string.leaderboards_rank_name,
                entry.rank ?: 0,
                // The character name is admin-configurable — a shard can publish
                // standings without naming who holds them, so a nameless rank is a
                // valid row rather than a broken one.
                entry.name ?: stringResource(R.string.leaderboards_hidden_name),
            ),
            style = MaterialTheme.typography.bodyMedium,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            modifier = Modifier.weight(1f),
        )
        Text(
            text = (entry.points ?: 0L).toString(),
            style = MaterialTheme.typography.bodyMedium,
            fontWeight = FontWeight.Medium,
            color = MaterialTheme.colorScheme.onSurfaceVariant,
        )
    }
}

/**
 * A board's display name: the shard's own literal when it has one, else the humanised
 * `PointsType` key. The fallback is the PRIMARY path — four of five boards on a real
 * shard name themselves with a cliloc and send `nameString: null`.
 */
internal fun boardLabel(board: PointsBoardDto): String {
    board.nameString?.takeIf { it.isNotBlank() }?.let { return it }
    return board.system.orEmpty()
        .replace(Regex("([a-z0-9])([A-Z])"), "$1 $2")
        .replaceFirstChar { it.uppercaseChar() }
}

/**
 * The name to stand in for an empty board: this instance's, falling back to the app
 * name — the same resolution the app bar uses, so a shard that publishes no branding
 * still reads as *something* rather than as a blank row.
 *
 * Pure and separate so the fallback order is testable; [BrandDto.name] can be present
 * but blank, which is a shard that set the key and left it empty.
 */
@Composable
internal fun placeholderName(brand: BrandDto?): String =
    brand?.name?.takeIf { it.isNotBlank() } ?: stringResource(R.string.app_name)
