//
//  Engagement.swift
//  lichunWebsocket
//
//  Wave 2 fun/balance models: deeper quest-loop engagement (full-clear streak +
//  weekly challenge), the achievement collection summary (per-category locked +
//  unlocked entries with hints), and the static player-initiated activity catalog.
//
//  Synced with backend shapes:
//    - questEngagement (server/src/services/retention/dailyQuests.ts
//        PersistedQuestEngagement + ActiveWeeklyChallenge) — rides in playerObject.
//    - AchievementSummary / CategorySummary / CollectionEntry
//        (server/src/services/retention/achievements.ts) — rides inside the
//        `achievementsList` message as `summary`.
//    - PLAYER_ACTIVITIES (server/src/game/engine/intradayActivity.ts) — mirrored
//        client-side as a static catalog for the performActivity action UI.
//
//  All structs are tolerant of absent fields so older / partial payloads decode.
//

import Foundation
import SwiftUI

// MARK: - Quest Engagement (deeper quest loop)

/// Snapshot of the deeper quest loop. `chains` are intentionally day-scoped and
/// NOT persisted in the player blob, so this snapshot carries the streak + the
/// rolled weekly challenge. Chains are surfaced live via quest-progress messages.
struct QuestEngagementSnapshot: Codable, Equatable {
    var fullClearStreak: Int = 0
    var lastFullClearDate: String?
    var lastStreakBonusDate: String?
    var weekly: WeeklyChallenge?

    /// Number of consecutive full-clear days that earns the streak bonus, and the
    /// diamond payout. Mirrors STREAK_BONUS_THRESHOLD / STREAK_BONUS_REWARD on the
    /// server. Used for the "N more days for a bonus" hint.
    static let streakBonusThreshold = 3
    static let streakBonusReward = 10

    enum CodingKeys: String, CodingKey {
        case fullClearStreak, lastFullClearDate, lastStreakBonusDate, weekly
    }

    init() {}

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        fullClearStreak = (try? c.decodeIfPresent(Int.self, forKey: .fullClearStreak)) ?? 0
        lastFullClearDate = try? c.decodeIfPresent(String.self, forKey: .lastFullClearDate)
        lastStreakBonusDate = try? c.decodeIfPresent(String.self, forKey: .lastStreakBonusDate)
        weekly = try? c.decodeIfPresent(WeeklyChallenge.self, forKey: .weekly)
    }

    /// Days remaining until the next streak bonus fires (1...threshold). When the
    /// streak is a clean multiple of the threshold, a full cycle remains.
    var daysToNextBonus: Int {
        let t = Self.streakBonusThreshold
        guard t > 0 else { return 0 }
        let remainder = fullClearStreak % t
        return remainder == 0 ? t : t - remainder
    }
}

/// The rolled weekly challenge — a larger payoff than a daily quest, persisted
/// for the week. Matches the server's ActiveWeeklyChallenge.
struct WeeklyChallenge: Codable, Equatable, Identifiable {
    var id: String = ""
    var questType: String = ""
    var description: String = ""
    var progress: Int = 0
    var progressRequired: Int = 0
    var diamondReward: Int = 0
    var completed: Bool = false
    var claimed: Bool = false
    var weekKey: String = ""
    var iconName: String = ""

    enum CodingKeys: String, CodingKey {
        case id, questType, description, progress, progressRequired
        case diamondReward, completed, claimed, weekKey, iconName
    }

    init() {}

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        id = (try? c.decodeIfPresent(String.self, forKey: .id)) ?? ""
        questType = (try? c.decodeIfPresent(String.self, forKey: .questType)) ?? ""
        description = (try? c.decodeIfPresent(String.self, forKey: .description)) ?? ""
        progress = (try? c.decodeIfPresent(Int.self, forKey: .progress)) ?? 0
        progressRequired = (try? c.decodeIfPresent(Int.self, forKey: .progressRequired)) ?? 0
        diamondReward = (try? c.decodeIfPresent(Int.self, forKey: .diamondReward)) ?? 0
        completed = (try? c.decodeIfPresent(Bool.self, forKey: .completed)) ?? false
        claimed = (try? c.decodeIfPresent(Bool.self, forKey: .claimed)) ?? false
        weekKey = (try? c.decodeIfPresent(String.self, forKey: .weekKey)) ?? ""
        iconName = (try? c.decodeIfPresent(String.self, forKey: .iconName)) ?? ""
    }

    var progressFraction: Double {
        guard progressRequired > 0 else { return completed ? 1.0 : 0.0 }
        return min(Double(progress) / Double(progressRequired), 1.0)
    }

    var progressText: String { "\(min(progress, progressRequired))/\(progressRequired)" }

    /// A non-empty SF Symbol name; the server icon if present, else a sensible default.
    var resolvedIcon: String { iconName.isEmpty ? "calendar.badge.exclamationmark" : iconName }

    var canClaim: Bool { completed && !claimed }
}

// MARK: - Achievement Collection Summary

/// Per-category achievement collection summary, surfaced inside the
/// `achievementsList` message as `summary`. Drives the collection / trophy-case
/// screen with locked + unlocked entries, hints, and completion percentages.
struct AchievementSummary: Codable, Equatable {
    var total: Int = 0
    var unlocked: Int = 0
    var progressPercent: Int = 0
    /// Category key -> summary. Keys: life_milestone, career, relationship,
    /// collection, secret.
    var byCategory: [String: CategorySummary] = [:]

    enum CodingKeys: String, CodingKey {
        case total, unlocked, progressPercent, byCategory
    }

    init() {}

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        total = (try? c.decodeIfPresent(Int.self, forKey: .total)) ?? 0
        unlocked = (try? c.decodeIfPresent(Int.self, forKey: .unlocked)) ?? 0
        progressPercent = (try? c.decodeIfPresent(Int.self, forKey: .progressPercent)) ?? 0
        byCategory = (try? c.decodeIfPresent([String: CategorySummary].self, forKey: .byCategory)) ?? [:]
    }

    /// Categories in display order, filtered to those the payload actually carries.
    var orderedCategories: [(key: String, summary: CategorySummary)] {
        let order = ["life_milestone", "career", "relationship", "collection", "secret"]
        return order.compactMap { key in
            guard let summary = byCategory[key] else { return nil }
            return (key, summary)
        }
    }
}

struct CategorySummary: Codable, Equatable {
    var total: Int = 0
    var unlocked: Int = 0
    var progressPercent: Int = 0
    var entries: [CollectionEntry] = []

    enum CodingKeys: String, CodingKey {
        case total, unlocked, progressPercent, entries
    }

    init() {}

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        total = (try? c.decodeIfPresent(Int.self, forKey: .total)) ?? 0
        unlocked = (try? c.decodeIfPresent(Int.self, forKey: .unlocked)) ?? 0
        progressPercent = (try? c.decodeIfPresent(Int.self, forKey: .progressPercent)) ?? 0
        entries = (try? c.decodeIfPresent([CollectionEntry].self, forKey: .entries)) ?? []
    }

    var progressFraction: Double { Double(min(max(progressPercent, 0), 100)) / 100.0 }
}

struct CollectionEntry: Codable, Equatable, Identifiable {
    var key: String = ""
    var name: String = ""
    var description: String = ""
    var icon: String = ""
    var reward: Int = 0
    var category: String = ""
    var unlocked: Bool = false
    var hidden: Bool = false
    var hint: String = ""
    var progressPercent: Int = 0

    var id: String { key }

    enum CodingKeys: String, CodingKey {
        case key, name, description, icon, reward, category, unlocked, hidden, hint, progressPercent
    }

    init() {}

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        key = (try? c.decodeIfPresent(String.self, forKey: .key)) ?? ""
        name = (try? c.decodeIfPresent(String.self, forKey: .name)) ?? ""
        description = (try? c.decodeIfPresent(String.self, forKey: .description)) ?? ""
        icon = (try? c.decodeIfPresent(String.self, forKey: .icon)) ?? ""
        reward = (try? c.decodeIfPresent(Int.self, forKey: .reward)) ?? 0
        category = (try? c.decodeIfPresent(String.self, forKey: .category)) ?? ""
        unlocked = (try? c.decodeIfPresent(Bool.self, forKey: .unlocked)) ?? false
        hidden = (try? c.decodeIfPresent(Bool.self, forKey: .hidden)) ?? false
        hint = (try? c.decodeIfPresent(String.self, forKey: .hint)) ?? ""
        progressPercent = (try? c.decodeIfPresent(Int.self, forKey: .progressPercent)) ?? 0
    }

    /// What to show as the title: hidden + locked achievements stay a mystery.
    var displayName: String { (hidden && !unlocked) ? "???" : name }

    /// What to show as the subtitle: the hint for locked, the description for unlocked.
    var displaySubtitle: String {
        if unlocked { return description }
        return hint.isEmpty ? description : hint
    }

    var resolvedIcon: String {
        if hidden && !unlocked { return "questionmark.circle" }
        return icon.isEmpty ? "trophy" : icon
    }
}

/// Friendly display labels for the server's achievement category keys.
enum AchievementCategoryDisplay {
    static func title(for key: String) -> String {
        switch key {
        case "life_milestone": return "Life Milestones"
        case "career": return "Career"
        case "relationship": return "Relationships"
        case "collection": return "Collection"
        case "secret": return "Secret"
        default: return key.replacingOccurrences(of: "_", with: " ").capitalized
        }
    }

    static func icon(for key: String) -> String {
        switch key {
        case "life_milestone": return "star.fill"
        case "career": return "briefcase.fill"
        case "relationship": return "heart.fill"
        case "collection": return "square.grid.2x2.fill"
        case "secret": return "lock.fill"
        default: return "trophy.fill"
        }
    }
}

// MARK: - Player Activity Catalog (performActivity command)

/// Client-side mirror of the server PLAYER_ACTIVITIES catalog. Used to render the
/// performActivity action UI: the activity's title, energy cost, rough effect
/// blurb, and an SF Symbol. The `id` is what the client sends in the
/// `performActivity` payload.
struct PlayerActivityInfo: Identifiable {
    let id: String
    let title: String
    let blurb: String
    let energyCost: Int
    let minAge: Int
    let icon: String
    let color: Color

    /// The catalog, mirroring server/src/game/engine/intradayActivity.ts
    /// PLAYER_ACTIVITIES. Effect blurbs summarize the numeric deltas.
    static let all: [PlayerActivityInfo] = [
        PlayerActivityInfo(
            id: "study",
            title: "Study",
            blurb: "Sharpen your mind. +Intelligence, a little more stress.",
            energyCost: 10,
            minAge: 0,
            icon: "book.fill",
            color: AppColors.intelligence
        ),
        PlayerActivityInfo(
            id: "exercise",
            title: "Exercise",
            blurb: "Get moving. +Health, -Stress.",
            energyCost: 15,
            minAge: 0,
            icon: "figure.run",
            color: AppColors.health
        ),
        PlayerActivityInfo(
            id: "socialize",
            title: "Socialize",
            blurb: "Call a friend. +Social, +Happiness, -Stress.",
            energyCost: 8,
            minAge: 0,
            icon: "person.2.fill",
            color: AppColors.friend
        ),
        PlayerActivityInfo(
            id: "sideHustle",
            title: "Side Hustle",
            blurb: "Earn extra cash. +Money, a little more stress. (Age 14+)",
            energyCost: 18,
            minAge: 14,
            icon: "dollarsign.circle.fill",
            color: AppColors.money
        ),
        PlayerActivityInfo(
            id: "hobby",
            title: "Hobby",
            blurb: "Make something. +Creativity, +Happiness, -Stress.",
            energyCost: 6,
            minAge: 0,
            icon: "paintbrush.fill",
            color: AppColors.happiness
        ),
    ]
}
