/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.data.api.dto

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject

/**
 * DTOs for a player's OWN game data (PLAN.md §6.3), read over the bearer-gated
 * `/player/shard/…` surface. The roster / char / vendor reads return the sidecar
 * payload verbatim (a permissive object), so only the fields the app renders are
 * modeled — unknown keys are ignored by the JSON parser (matching the website's
 * `CharacterSheet.jsx` / `GameAccounts.jsx` and `docs/link/INTEGRATION.md` §5).
 * Presentation is text-only for v1 (no item icons / paperdoll).
 *
 * In-game serials are hex strings (e.g. "0x24C"), the same opaque-key form used on
 * the public boards (`ShardDto.ActorDto`/`ChampDto`/`HouseDto`) — never numbers.
 */

// ── Game-account linking ─────────────────────────────────────────────────────

/** `GET /player/shard/accounts` — a linked in-game account. */
@Serializable
data class ShardLinkDto(
    val account: String = "",
    val userId: Long? = null,
    val charName: String? = null,
    val linkedAt: String? = null,
)

/** `POST /player/shard/link` body — the one-time code shown by `[link` in game. */
@Serializable
data class ShardLinkRequest(val code: String)

/** `POST /player/shard/link` result — the confirmed link. */
@Serializable
data class ShardLinkResultDto(
    val linked: Boolean = false,
    val account: String? = null,
)

/** `POST /player/shard/account` (hybrid signup) body. */
@Serializable
data class CreateGameAccountRequest(
    val account: String,
    val password: String,
)

// ── Character roster + sheet ─────────────────────────────────────────────────

/** `GET /player/shard/roster/:account` — the account's characters (incl. offline). */
@Serializable
data class RosterDto(
    val acct: String? = null,
    val chars: List<RosterCharDto> = emptyList(),
)

/** One character in a roster; the picker fetches the full sheet on demand. */
@Serializable
data class RosterCharDto(
    val slot: Int? = null,
    val serial: String = "",
    val name: String? = null,
    val body: Int? = null,
    val online: Boolean = false,
)

/**
 * `GET /player/shard/char/:serial` — a character sheet. `guild` / `governorOf` are
 * best-effort cross-links the backend decorates in (never fail the sheet).
 */
@Serializable
data class CharProfileDto(
    val serial: String? = null,
    val name: String? = null,
    val title: String? = null,
    val online: Boolean = false,
    val acct: String? = null,
    val stats: CharStatsDto? = null,
    val skills: List<SkillDto> = emptyList(),
    val equipment: List<EquipmentDto> = emptyList(),
    val titles: TitlesDto? = null,
    val guild: GuildRefDto? = null,
    val governorOf: List<String> = emptyList(),
    /**
     * Loyalty / points standings (Protocol 3.0 §7.3). Empty for a character that has
     * earned nothing anywhere — the shard omits systems the character has no entry in
     * — and empty on a shard whose plugin predates 3.0.
     *
     * Served **ungated**: a character's own standings are self-service data on
     * `/player/shard/char/:serial` and do not depend on the public `leaderboards`
     * feature being visible. Don't re-gate them app-side.
     */
    val points: List<CharPointsDto> = emptyList(),
)

/**
 * One point system a character holds a score in (Protocol 3.0 §7.3).
 *
 * Three shapes here are counter-intuitive, and all three are what a REAL shard sends
 * (`docs/link/v3.md` §7.5 — a fake shard emits whatever the spec says it should):
 *
 *  - **[maxPoints] `0` means UNCAPPED, and is the common case**, not an edge case.
 *    ServUO's idiom for an uncapped system is `double.MaxValue`, which the plugin
 *    normalises to `0` because the C# cast is unchecked and yielded `long.MinValue`.
 *    Nothing may divide by it, and a full-width progress bar for an uncapped score
 *    would imply a completion that doesn't exist.
 *  - **[nameString] is usually `null`.** Most systems name themselves with a cliloc
 *    rather than a literal, so humanising [system] (`QueensLoyalty` → "Queens
 *    Loyalty") is the PRIMARY display path, not a defensive fallback.
 *  - **[rank] is absent unless the shard runs `Bridge.cfg PointsProfileRank=true`.**
 *    Absent and "unranked" are different answers, so it renders only when sent.
 */
@Serializable
data class CharPointsDto(
    val system: String? = null,
    val nameString: String? = null,
    val points: Long? = null,
    val maxPoints: Long? = null,
    val rank: Int? = null,
) {
    /** The cap, or null when the system is uncapped (see [maxPoints]). */
    val cap: Long? get() = maxPoints?.takeIf { it > 0 }
}

@Serializable
data class CharStatsDto(
    val str: Int? = null,
    val dex: Int? = null,
    val int: Int? = null,
    val hits: Int? = null,
    val hitsMax: Int? = null,
    val mana: Int? = null,
    val manaMax: Int? = null,
    val stam: Int? = null,
    val stamMax: Int? = null,
    val resist: ResistDto? = null,
)

@Serializable
data class ResistDto(
    val phys: Int? = null,
    val fire: Int? = null,
    val cold: Int? = null,
    val pois: Int? = null,
    val energy: Int? = null,
)

/**
 * A skill line. `base` is the trained value, `value` includes item/temp bonuses,
 * `cap` is the cap — do NOT assume base ≤ cap (GM chars exceed it). Doubles, as the
 * shard reports tenths.
 */
@Serializable
data class SkillDto(
    val n: String? = null,
    val base: Double? = null,
    val value: Double? = null,
    val cap: Double? = null,
)

/**
 * An equipped item. Names are usually clilocs (numeric), not strings, and the app
 * ships no cliloc table, so the text-only sheet renders layer + id + hue + mods.
 * [mods] is a flattened map of non-zero AOS attributes (empty for plain items);
 * kept as a raw object since values may be numbers or strings.
 */
@Serializable
data class EquipmentDto(
    val serial: String? = null,
    val layer: String? = null,
    val itemId: Int? = null,
    val hue: Int? = null,
    val mods: JsonObject? = null,
    /**
     * A player-given name — set for the minority of items someone has renamed, null
     * for almost everything else. The shard sends the plain `Item.Name` field; it
     * never builds a display name (that call is a packet builder, not a field read).
     */
    val name: String? = null,
    /**
     * The item's type name, resolved from its cliloc id **by the website** against
     * its own table (`docs/website/CLILOCS.md`). Null on a shard that has no cliloc
     * table configured, which is fully supported — the sheet then falls back to the
     * layer, exactly as it did before the table existed.
     */
    val clilocName: String? = null,
) {
    /**
     * What to call this item.
     *
     * A player-given [name] outranks the resolved type name — "Bob's lucky axe" must
     * not be relabelled "hatchet" — and the server applies the same precedence, so
     * this only re-states it for an item that arrived with both.
     */
    val label: String? get() = name ?: clilocName ?: layer
}

/**
 * Display titles (Protocol 2.0). `selected` is the index into [reward] currently
 * shown (-1 if none); [reward] entries may be a cliloc number-as-string or a literal.
 *
 * [rewardResolved] is the website's **parallel array** with the numeric entries turned
 * into words against its cliloc table — same length and order as [reward], with a null
 * where an id resolved to nothing. It is absent entirely when no entry was numeric or
 * the shard has no cliloc table, so read it positionally and tolerate it being short.
 * See `displayTitles` in the character sheet.
 */
@Serializable
data class TitlesDto(
    val selected: Int? = null,
    val reward: List<String> = emptyList(),
    val rewardResolved: List<String?> = emptyList(),
    val fameKarma: String? = null,
    val skill: String? = null,
)

/** The guild a character leads (cross-linked from board data). */
@Serializable
data class GuildRefDto(
    val name: String? = null,
    val abbr: String? = null,
)

// ── Player vendors + sales ───────────────────────────────────────────────────

/** `GET /player/shard/vendors/:account` — every player vendor on the account. */
@Serializable
data class VendorSnapshotDto(
    val acct: String? = null,
    val vendors: List<VendorDto> = emptyList(),
)

@Serializable
data class VendorDto(
    val serial: String? = null,
    val shopName: String? = null,
    val holdGold: Long? = null,
    val ownerSerial: String? = null,
    val map: String? = null,
    val x: Int? = null,
    val y: Int? = null,
    val listings: List<VendorListingDto> = emptyList(),
)

/** A single vendor listing. Item names are clilocs (see [EquipmentDto]); text-only. */
@Serializable
data class VendorListingDto(
    val serial: String? = null,
    val itemId: Int? = null,
    val amount: Int? = null,
    val price: Long? = null,
    val forSale: Boolean = false,
)

/** `GET /player/shard/sales` — a player-vendor sale, visible only to the owner. */
@Serializable
data class VendorSaleDto(
    /** Sale time, epoch ms. */
    val t: Long? = null,
    val itemType: String? = null,
    val amount: Int? = null,
    val price: Long? = null,
    val commission: Int? = null,
    val ownerAcct: String? = null,
)

// ── Player houses ────────────────────────────────────────────────────────────

/**
 * `GET /player/shard/houses` — the caller's OWN houses, with full decay/IDOC
 * detail (their own property). Serial is a hex string here.
 */
@Serializable
data class PlayerHouseDto(
    val serial: String = "",
    val stage: String? = null,
    val map: String? = null,
    val x: Int? = null,
    val y: Int? = null,
    val z: Int? = null,
    val region: String? = null,
    val name: String? = null,
    val ownerSerial: String? = null,
    val ownerAcct: String? = null,
    val builtOn: String? = null,
    val lastRefreshed: String? = null,
    val isIdoc: Boolean = false,
    val updatedAt: String? = null,
)
