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

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.input.ImeAction
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.MarketListingDto
import com.runicgateway.app.data.api.dto.MarketLocationDto
import com.runicgateway.app.data.api.dto.MarketVendorDto
import com.runicgateway.app.ui.UiState
import com.runicgateway.app.ui.components.EmptyView
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

/**
 * The shard-wide marketplace (PLAN.md §9 M11): search every player vendor's stock.
 *
 * The staleness line under the search box is required, not decoration — see
 * [MarketViewModel]. Results are listings, so a row names both the item and the shop
 * that sells it, and tapping it opens that shop.
 */
@Composable
fun MarketScreen(
    onOpenVendor: (String) -> Unit,
    modifier: Modifier = Modifier,
    viewModel: MarketViewModel = hiltViewModel(),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val meta by viewModel.meta.collectAsStateWithLifecycle()
    val query by viewModel.query.collectAsStateWithLifecycle()

    Column(modifier.fillMaxSize()) {
        OutlinedTextField(
            value = query,
            onValueChange = viewModel::onQueryChange,
            label = { Text(stringResource(R.string.market_search_label)) },
            singleLine = true,
            // Searched on submit rather than per keystroke: this is the site's first
            // rate-limited public endpoint.
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
            keyboardActions = KeyboardActions(onSearch = { viewModel.search() }),
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
        )
        meta?.staleAt?.let {
            SectionLabel(
                text = stringResource(R.string.market_staleness),
                modifier = Modifier.padding(horizontal = 16.dp),
            )
        }

        when (val s = state) {
            is UiState.Loading -> LoadingView()
            is UiState.Error -> ErrorView(s.kind, onRetry = viewModel::load)
            is UiState.Success -> {
                if (s.data.listings.isEmpty()) {
                    EmptyView(stringResource(R.string.market_empty))
                } else {
                    LazyColumn(
                        modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
                        verticalArrangement = Arrangement.spacedBy(8.dp),
                        contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),
                    ) {
                        items(s.data.listings, key = { it.serial ?: it.hashCode().toString() }) { listing ->
                            ListingCard(listing, onOpenVendor)
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun ListingCard(listing: MarketListingDto, onOpenVendor: (String) -> Unit) {
    val vendorSerial = listing.vendor?.serial
    ShardCard(
        Modifier
            .fillMaxWidth()
            .then(if (vendorSerial != null) Modifier.clickable { onOpenVendor(vendorSerial) } else Modifier),
    ) {
        Column(Modifier.padding(16.dp)) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                Text(
                    text = listingTitle(listing)
                        ?: stringResource(R.string.market_unnamed_item, listing.itemId ?: 0),
                    style = MaterialTheme.typography.titleSmall,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.weight(1f),
                )
                Text(
                    text = stringResource(R.string.market_price, listing.price ?: 0L),
                    style = MaterialTheme.typography.titleSmall,
                    fontWeight = FontWeight.Medium,
                )
            }
            val shop = listing.vendor?.shopName ?: listing.vendor?.ownerName
            if (shop != null) {
                Text(
                    text = shop,
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
            locationLine(listing.vendor?.location)?.let {
                Text(
                    text = it,
                    style = MaterialTheme.typography.labelSmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                )
            }
        }
    }
}

/**
 * One shop and its stock. The only surface that can answer the two questions a result
 * list can't: how much of a truncated shop is published, and where a shop is when the
 * shard doesn't say.
 */
@Composable
fun MarketVendorScreen(
    serial: String,
    modifier: Modifier = Modifier,
    viewModel: MarketVendorViewModel = hiltViewModel(),
) {
    androidx.compose.runtime.LaunchedEffect(serial) { viewModel.load(serial) }
    val state by viewModel.state.collectAsStateWithLifecycle()

    when (val s = state) {
        is UiState.Loading -> LoadingView(modifier)
        is UiState.Error -> ErrorView(s.kind, onRetry = viewModel::retry, modifier = modifier)
        is UiState.Success -> VendorContent(s.data, modifier)
    }
}

@Composable
private fun VendorContent(vendor: MarketVendorDto, modifier: Modifier = Modifier) {
    LazyColumn(
        modifier = modifier.fillMaxSize().padding(horizontal = 16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 16.dp),
    ) {
        item {
            Column {
                Text(
                    vendor.shopName ?: stringResource(R.string.market_unnamed_shop),
                    style = MaterialTheme.typography.titleLarge,
                )
                vendor.ownerName?.let {
                    Text(
                        stringResource(R.string.market_owner, it),
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
                Text(
                    // A gated location is a real answer, not a blank: the shard has
                    // this shop, it just doesn't publish where it stands.
                    text = locationLine(vendor.location) ?: stringResource(R.string.market_location_hidden),
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                )
                if (vendor.truncated) {
                    Text(
                        text = stringResource(
                            R.string.market_truncated,
                            vendor.count ?: vendor.items.size,
                            vendor.total ?: 0,
                        ),
                        style = MaterialTheme.typography.labelSmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        modifier = Modifier.padding(top = 6.dp),
                    )
                }
            }
        }
        if (vendor.items.isEmpty()) {
            item { Text(stringResource(R.string.market_shop_empty), style = MaterialTheme.typography.bodyMedium) }
        } else {
            items(vendor.items, key = { it.serial ?: it.hashCode().toString() }) { item ->
                Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text(
                        text = listingTitle(item) ?: stringResource(R.string.market_unnamed_item, item.itemId ?: 0),
                        style = MaterialTheme.typography.bodyMedium,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis,
                        modifier = Modifier.weight(1f),
                    )
                    Text(
                        text = stringResource(R.string.market_price, item.price ?: 0L),
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
            }
        }
    }
}

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

/**
 * What to call a listing: a player-set name, else the server-resolved cliloc name,
 * else null so the caller can fall back to the item id. A shard with no cliloc table
 * configured legitimately publishes neither.
 *
 * A stack shows its count, since "12 × ingot" and "ingot" at the same price are very
 * different offers.
 */
internal fun listingTitle(listing: MarketListingDto): String? {
    val base = listing.label ?: return null
    val amount = listing.amount ?: 1
    return if (amount > 1) "$amount × $base" else base
}

/**
 * A shop's whereabouts as one line, or null when the shard publishes no location —
 * which happens both because an admin gated the field and because the nesting means
 * the WHOLE block goes at once, never a half-populated one.
 */
internal fun locationLine(location: MarketLocationDto?): String? {
    if (location == null) return null
    val place = location.house ?: location.region
    val facet = location.map
    return when {
        place != null && facet != null -> "$place, $facet"
        place != null -> place
        facet != null -> facet
        else -> null
    }
}
