import Foundation
import Combine

/// Manages core game state: player, person, swipe character, extracurriculars,
/// date ideas, and the lightweight "u" update, "playerObject", "personObject",
/// "getSwipeCharacter", and "extraCurriculars" message handlers.
class GameStateStore: ObservableObject {
    @Published var player: Player = Player()
    @Published var person: Person = Person()
    @Published var swipeCharacter: Person? = Person()
    @Published var extracurriculars: [ExtracurricularClass] = []
    @Published var dateIdeas: [DateIdea] = []

    /// Set when an offline welcome-back digest arrives (via `offlineDigest`
    /// message or inside `playerObject.offlineStats.digest`). Drives the
    /// one-time OfflineDigestView sheet in ContentView. Cleared after shown.
    @Published var pendingOfflineDigest: OfflineDigest?

    /// Transient stat/resource changes derived from incoming `"u"` lightweight
    /// updates. Drives the floating "+5 energy" / "-3 money" overlays so gains
    /// and losses feel earned. Each item self-expires; ContentView observes this
    /// array and renders a FloatingDeltaView per entry. (T014: floating deltas)
    @Published var statDeltas: [StatDelta] = []

    /// Emit a floating delta and auto-prune it after its lifetime so the array
    /// never grows unbounded during fast-forward play.
    private func emitDelta(_ kind: StatDelta.Kind, _ amount: Int) {
        guard amount != 0 else { return }
        let delta = StatDelta(kind: kind, amount: amount)
        statDeltas.append(delta)
        // Cap retained deltas to avoid runaway accumulation at high speed.
        if statDeltas.count > 8 {
            statDeltas.removeFirst(statDeltas.count - 8)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { [weak self] in
            self?.statDeltas.removeAll { $0.id == delta.id }
        }
    }

    weak var facade: WebSocketService?
    private var cancellables = Set<AnyCancellable>()
    private var playerNestedSub: AnyCancellable?
    private var personNestedSub: AnyCancellable?

    init() {
        // Forward nested Player/Person objectWillChange so that mutations
        // to e.g. person.diamonds or player.activeConversations propagate
        // through GameStateStore → facade → SwiftUI. Re-subscribes
        // automatically when the reference is reassigned.
        $player.sink { [weak self] newPlayer in
            self?.playerNestedSub = newPlayer.objectWillChange.sink { [weak self] _ in
                self?.objectWillChange.send()
            }
        }.store(in: &cancellables)

        $person.sink { [weak self] newPerson in
            self?.personNestedSub = newPerson.objectWillChange.sink { [weak self] _ in
                self?.objectWillChange.send()
            }
        }.store(in: &cancellables)
    }

    // MARK: - Message Handling

    func handleMessage(type: String, data: [String: Any]) -> Bool {
        switch type {
        case "u":
            handleLightweightUpdate(data)
            return true
        case "playerObject":
            handlePlayerObject(data)
            return true
        case "personObject":
            handlePersonObject(data)
            return true
        case "getSwipeCharacter":
            handleSwipeCharacter(data)
            return true
        case "extraCurriculars":
            handleExtraCurriculars(data)
            return true
        case "lifeSummaryEvent":
            handleLifeSummaryEvent(data)
            return true
        case "offlineDigest":
            handleOfflineDigest(data)
            return true
        case "lifeGoalsUpdate":
            handleLifeGoalsUpdate(data)
            return true
        case "activityPerformed", "activityPlanned":
            handleActivityResult(type: type, data: data)
            return true
        case "habitQuitting", "habitQuitStopped", "jobApplied", "jobQuit",
             "extracurricularApplied", "extracurricularQuit", "focusUpdated",
             "relationshipEnded", "romanceStarted", "debugSetupComplete", "debugGrantComplete":
            handleActionConfirmation(type: type, data: data)
            return true
        default:
            return false
        }
    }

    /// `activityPerformed` / `activityPlanned`: confirmation of a player-initiated
    /// performActivity command. The authoritative stat/energy/money changes ride
    /// in the playerObject the server also sends; here we just surface the toast.
    private func handleActivityResult(type: String, data: [String: Any]) {
        let success = data["success"] as? Bool ?? true
        guard success else {
            let message = data["message"] as? String ?? "Couldn't do that activity right now."
            ToastManager.shared.show(.warning, message: message)
            return
        }
        let title = data["title"] as? String ?? data["name"] as? String
        let message = data["message"] as? String
            ?? title
            ?? (type == "activityPlanned" ? "Activity planned." : "Activity done.")
        ToastManager.shared.show(.success, message: message)
        SoundManager.shared.playSound(.success)
    }

    private func handleActionConfirmation(type: String, data: [String: Any]) {
        let success = data["success"] as? Bool ?? true
        guard success else { return }

        let message = data["message"] as? String ?? defaultActionMessage(for: type)
        ToastManager.shared.show(.success, message: message)
    }

    private func defaultActionMessage(for type: String) -> String {
        switch type {
        case "habitQuitting":
            return "Started working on breaking this habit."
        case "habitQuitStopped":
            return "Stopped trying to break this habit."
        case "jobApplied":
            return "Job application submitted."
        case "jobQuit":
            return "You left your job."
        case "extracurricularApplied":
            return "Enrolled in activity."
        case "extracurricularQuit":
            return "Left activity."
        case "focusUpdated":
            return "Focus updated."
        case "relationshipEnded":
            return "Relationship ended."
        case "romanceStarted":
            return "Romance started."
        case "debugSetupComplete":
            return "Debug preset applied."
        case "debugGrantComplete":
            return "Debug resources applied."
        default:
            return "Action completed."
        }
    }

    // MARK: - Private Handlers

    private func handleLightweightUpdate(_ data: [String: Any]) {
        // Guard no-op updates to avoid unnecessary SwiftUI redraws
        if let date = data["date"] as? String, self.player.date != date {
            self.player.date = date
        }
        if let minuteOfHour = data["minuteOfHour"] as? Int, self.player.minuteOfHour != minuteOfHour {
            self.player.minuteOfHour = minuteOfHour
        }
        if let hourOfDay = data["hourOfDay"] as? Int, self.player.hourOfDay != hourOfDay {
            self.player.hourOfDay = hourOfDay
        }
        if let gameSpeed = data["gameSpeed"] as? Int, self.player.gameSpeed != gameSpeed {
            self.player.gameSpeed = gameSpeed
        }
        if let money = data["money"] as? Int, self.person.money != money {
            emitDelta(.money, money - self.person.money)
            self.person.money = money
        }
        if let diamonds = data["diamonds"] as? Int, self.person.diamonds != diamonds {
            emitDelta(.diamonds, diamonds - self.person.diamonds)
            self.person.diamonds = diamonds
        }
        if let location = data["location"] as? String, self.person.location != location {
            self.person.location = location
        }
        if let calcEnergy = data["calcEnergy"] as? Int, self.person.calcEnergy != calcEnergy {
            emitDelta(.energy, calcEnergy - self.person.calcEnergy)
            self.person.calcEnergy = calcEnergy
        }
        if let status = data["status"] as? String, self.person.status != status {
            self.person.status = status
        }
        if let intraDayMessage = data["intraDayMessage"] as? String, self.person.intraDayMessage != intraDayMessage {
            self.person.intraDayMessage = intraDayMessage
        }
        if let mood = data["mood"] as? String, self.person.mood != mood {
            self.person.mood = mood
        }
        if let hunger = data["hunger"] as? Int, self.person.hunger != hunger {
            self.person.hunger = hunger
        }
        if let thirst = data["thirst"] as? Int, self.person.thirst != thirst {
            self.person.thirst = thirst
        }
        if let health = data["health"] as? Int, self.person.health != health {
            emitDelta(.health, health - self.person.health)
            self.person.health = health
        }
        if let healthDouble = data["health"] as? Double {
            let healthInt = Int(healthDouble)
            if self.person.health != healthInt {
                emitDelta(.health, healthInt - self.person.health)
                self.person.health = healthInt
            }
        }
        if let happiness = data["happiness"] as? Int, self.person.happiness != happiness {
            emitDelta(.happiness, happiness - self.person.happiness)
            self.person.happiness = happiness
        }
        if let stress = data["stress"] as? Int, self.person.stress != stress {
            self.person.stress = stress
        }
        if let intelligence = data["intelligence"] as? Int, self.person.intelligence != intelligence {
            self.person.intelligence = intelligence
        }
        if let ageYears = data["ageYears"] as? Int, self.person.ageYears != ageYears {
            self.person.ageYears = ageYears
        }
    }

    private func handlePlayerObject(_ data: [String: Any]) {
        self.player.date = data["date"] as? String ?? ""
        self.player.gameSpeed = data["gameSpeed"] as? Int ?? 0
        self.player.status = data["status"] as? String ?? ""
        self.player.season = data["season"] as? String ?? ""

        if let focusAny = data["focuses"] {
            if let focusArray = focusAny as? [[String: Any]] {
                let jsonData = try? JSONSerialization.data(withJSONObject: focusArray)
                let decoder = JSONDecoder()
                if let d = jsonData, let decoded = try? decoder.decode([FocusOption].self, from: d) {
                    self.player.focuses = decoded
                }
            }
        }
        if let storeAny = data["storeItems"] {
            if let storeArray = storeAny as? [[String: Any]] {
                let jsonData = try? JSONSerialization.data(withJSONObject: storeArray)
                let decoder = JSONDecoder()
                if let d = jsonData, let decoded = try? decoder.decode([StoreItem].self, from: d) {
                    self.player.storeItems = decoded
                }
            }
        }
        if let inAppPurchasesAny = data["inAppPurchases"] {
            if let inAppPurchases = inAppPurchasesAny as? [[String: Any]] {
                let jsonData = try? JSONSerialization.data(withJSONObject: inAppPurchases)
                let decoder = JSONDecoder()
                if let d = jsonData, let decoded = try? decoder.decode([InAppPurchaseItem].self, from: d) {
                    self.player.inAppPurchases = decoded
                }
            }
        }
        if let occupationsAny = data["occupations"] {
            if let occupations = occupationsAny as? [[String: Any]] {
                let jsonData = try? JSONSerialization.data(withJSONObject: occupations)
                let decoder = JSONDecoder()
                if let d = jsonData, let decoded = try? decoder.decode([OccupationClass].self, from: d) {
                    self.player.occupations = decoded
                }
            }
        }

        if let relationshipAny = data["relData"] {
            if let relData = relationshipAny as? [[String: Any]] {
                do {
                    let jsonData = try JSONSerialization.data(withJSONObject: relData)
                    let decoder = JSONDecoder()
                    self.player.relData = try decoder.decode([Relationship].self, from: jsonData)
                } catch {
                    #if DEBUG
                    print("Error decoding relData: \(error)")
                    #endif
                }
            }
        }

        if let dateAny = data["dateIdeas"] {
            if let dateIdeas = dateAny as? [[String: Any]] {
                do {
                    let jsonData = try JSONSerialization.data(withJSONObject: dateIdeas)
                    let decoder = JSONDecoder()
                    self.dateIdeas = try decoder.decode([DateIdea].self, from: jsonData)
                } catch {
                    #if DEBUG
                    print("Error decoding JSON: \(error)")
                    #endif
                }
            }
        }

        if let conversationArray = data["conversations"] as? [[String: Any]] {
            #if DEBUG
            print("Received \(conversationArray.count) conversations from server")
            #endif
            for (index, conversationData) in conversationArray.enumerated() {
                #if DEBUG
                print("Conversation \(index): id=\(conversationData["id"] ?? "nil"), char=\(conversationData["character"] ?? "nil"), msgCount=\((conversationData["conversation"] as? [Any])?.count ?? 0)")
                #endif
                do {
                    let jsonData = try JSONSerialization.data(withJSONObject: conversationData)

                    let decoder = JSONDecoder()
                    var newConversation = try decoder.decode(ConversationObj.self, from: jsonData)

                    // Memory optimization: Limit messages to 100 per conversation
                    if newConversation.conversation.count > 100 {
                        newConversation.conversation = Array(newConversation.conversation.suffix(100))
                    }

                    // Clear pending request status
                    if let characterId = newConversation.character {
                        facade?.clearPendingConversationRequest(forCharacter: characterId)
                    }

                    // Simple update: server data replaces existing
                    if let characterId = newConversation.character,
                       let index = self.player.activeConversations.firstIndex(where: { $0.character == characterId }) {
                        self.player.activeConversations[index] = newConversation
                    } else {
                        self.player.activeConversations.append(newConversation)
                    }
                } catch let DecodingError.dataCorrupted(context) {
                    #if DEBUG
                    print("Conversation decode - Data corrupted: \(context)")
                    print("Raw data: \(conversationData)")
                    #endif
                } catch let DecodingError.keyNotFound(key, context) {
                    #if DEBUG
                    print("Conversation decode - Key '\(key)' not found: \(context.debugDescription)")
                    print("codingPath: \(context.codingPath)")
                    print("Raw data: \(conversationData)")
                    #endif
                } catch let DecodingError.valueNotFound(value, context) {
                    #if DEBUG
                    print("Conversation decode - Value '\(value)' not found: \(context.debugDescription)")
                    print("codingPath: \(context.codingPath)")
                    print("Raw data: \(conversationData)")
                    #endif
                } catch let DecodingError.typeMismatch(type, context) {
                    #if DEBUG
                    print("Conversation decode - Type '\(type)' mismatch: \(context.debugDescription)")
                    print("codingPath: \(context.codingPath)")
                    print("Raw data: \(conversationData)")
                    #endif
                } catch {
                    #if DEBUG
                    print("Conversation decode failed: \(error)")
                    print("Raw data: \(conversationData)")
                    #endif
                }
            }
            #if DEBUG
            print("After processing: \(self.player.activeConversations.count) conversations in player.activeConversations")
            #endif
        } else if data["conversations"] != nil {
            #if DEBUG
            print("conversations key exists but is not [[String: Any]]: \(Swift.type(of: data["conversations"]!))")
            #endif
        }

        self.player.hourOfDay = data["hourOfDay"] as? Int ?? 0
        self.player.minuteOfHour = data["minuteOfHour"] as? Int ?? 0
        if let personData = data["c"] as? [String: Any] {
            self.person = parsePerson(from: personData)

            if let personListData = data["r"] as? [[String: Any]] {
                self.player.r = personListData.map { parsePerson(from: $0) }
            }

            if let storeAny = personData["items"] {
                if let storeArray = storeAny as? [[String: Any]] {
                    let jsonData = try? JSONSerialization.data(withJSONObject: storeArray)
                    let decoder = JSONDecoder()
                    if let d = jsonData, let decoded = try? decoder.decode([StoreItem].self, from: d) {
                        self.person.items = decoded
                    }
                }
            }
            if let habitsAny = personData["habits"] {
                if let habitArray = habitsAny as? [[String: Any]] {
                    let jsonData = try? JSONSerialization.data(withJSONObject: habitArray)
                    let decoder = JSONDecoder()
                    if let d = jsonData, let decoded = try? decoder.decode([Habit].self, from: d) {
                        self.person.habits = decoded
                    }
                }
            }

            if let activitiesData = personData["activities"] as? [[String: Any]] {
                self.person.activities = activitiesData.compactMap { parseActivity(from: $0) }
            }

            if let activityRecords = personData["activityRecords"] as? [[String: Any]] {
                self.person.activityRecords = activityRecords.compactMap { parseActivityRecord(from: $0) }
            }
        }

        // MARK: Lifecycle "spine" — top-level playerObject fields
        // lifeSummary is null on a living character; populated after death
        // (also surfaced live via lifeSummaryEvent). Decode tolerantly.
        if let summaryDict = data["lifeSummary"] as? [String: Any] {
            self.player.lifeSummary = Self.decode(LifeSummary.self, from: summaryDict)
        } else {
            self.player.lifeSummary = nil
        }

        self.player.familyPrestige = (data["familyPrestige"] as? Int)
            ?? Int(data["familyPrestige"] as? Double ?? 0)
        if let treeArray = data["familyTree"] as? [[String: Any]] {
            self.player.familyTree = Self.decode([FamilyTreeEntry].self, from: treeArray) ?? []
        }
        self.player.pendingInheritance = (data["pendingInheritance"] as? Int)
            ?? Int(data["pendingInheritance"] as? Double ?? 0)

        // Welcome-back digest can ride inside offlineStats.digest on connect.
        if let offlineStats = data["offlineStats"] as? [String: Any],
           let digestDict = offlineStats["digest"] as? [String: Any],
           let digest = Self.decode(OfflineDigest.self, from: digestDict) {
            self.player.offlineDigest = digest
            self.pendingOfflineDigest = digest
        }

        // Forward-looking life goals snapshot embedded in playerObject (Wave 1:
        // cache only). The dedicated lifeGoalsUpdate message keeps it fresh.
        if let goalsDict = data["lifeGoals"] as? [String: Any],
           let goals = Self.decode(LifeGoalsSnapshot.self, from: goalsDict) {
            self.player.lifeGoals = goals
        }

        // Deeper quest-loop engagement (full-clear streak + weekly challenge)
        // rides in playerObject. Chains are day-scoped and arrive via quest
        // progress messages, so they are not in this snapshot.
        if let engagementDict = data["questEngagement"] as? [String: Any],
           let engagement = Self.decode(QuestEngagementSnapshot.self, from: engagementDict) {
            self.player.questEngagement = engagement
        }
    }

    // MARK: - Lifecycle Handlers

    /// `lifeSummaryEvent`: `{ type, summary: LifeSummary }`. Sent the moment the
    /// character dies (online path) to power the DeathView recap + New Life flow.
    private func handleLifeSummaryEvent(_ data: [String: Any]) {
        if let summaryDict = data["summary"] as? [String: Any],
           let summary = Self.decode(LifeSummary.self, from: summaryDict) {
            self.player.lifeSummary = summary
            // Keep the generational layer in sync from the legacy payload so the
            // death screen + legacy tree reflect the just-completed life.
            if let legacy = summary.legacy {
                self.player.familyPrestige = legacy.familyPrestige
                if !legacy.familyTree.isEmpty {
                    self.player.familyTree = legacy.familyTree
                }
                self.player.pendingInheritance = legacy.inheritance
            }
        }

        // FU2 fix: transition to DeathView IMMEDIATELY on mid-session death.
        // The server sets c.status = "dead" server-side but only emits this
        // lifeSummaryEvent (not a fresh playerObject), so the live client never
        // learned the dead status and only flipped to DeathView after a
        // reconnect pushed a "dead" playerObject. Mirror that status flip here so
        // resolveAppRootDestination(...) routes to .death on the next render. The
        // lifeSummary needed by DeathView arrived in this same event, so the
        // recap renders right away. (assigning person.status publishes via the
        // nested objectWillChange forwarding wired up in init().)
        if self.person.status != "dead" {
            self.person.status = "dead"
        }
    }

    /// `offlineDigest`: flat `{ type, minutesAway, moneyDelta, ageYearsDelta,
    /// notableEvents, generatedAt }`. Drives the one-time welcome-back card.
    private func handleOfflineDigest(_ data: [String: Any]) {
        if let digest = Self.decode(OfflineDigest.self, from: data) {
            self.player.offlineDigest = digest
            self.pendingOfflineDigest = digest
        }
    }

    /// `lifeGoalsUpdate`: `{ type, active, completed, lifeScore, justCompleted }`.
    /// Wave 2: cache the snapshot AND surface any just-completed goals as a
    /// celebratory toast (the dedicated LifeGoalsView reads the cached snapshot).
    private func handleLifeGoalsUpdate(_ data: [String: Any]) {
        if let goals = Self.decode(LifeGoalsSnapshot.self, from: data) {
            self.player.lifeGoals = goals
            for goal in goals.justCompleted {
                let rewardSuffix = goal.reward > 0 ? " +$\(goal.reward)" : ""
                ToastManager.shared.show(
                    .success,
                    message: "Life Goal achieved: \(goal.title)!\(rewardSuffix)"
                )
                SoundManager.shared.playSound(.achievement)
            }
        }
    }

    /// Generic Foundation-JSON → Codable bridge used by the lifecycle handlers.
    private static func decode<T: Decodable>(_ type: T.Type, from object: Any) -> T? {
        guard let jsonData = try? JSONSerialization.data(withJSONObject: object) else {
            return nil
        }
        return try? JSONDecoder().decode(T.self, from: jsonData)
    }

    private func handlePersonObject(_ data: [String: Any]) {
        #if DEBUG
        print("received updated person")
        #endif
        let person = parsePerson(from: data)
        if let availableConversations = data["availableConversations"] as? [[String: Any]] {
            person.availableConversations = availableConversations.compactMap { conversationData in
                ConversationClass(fname: conversationData["fname"] as? String ?? "", button: conversationData["button"] as? String ?? "")
            }
        }
        if let index = self.player.r.firstIndex(where: { $0.id == person.id }) {
            self.player.r[index] = person
        }
        facade?.personReceived = true
    }

    private func handleSwipeCharacter(_ data: [String: Any]) {
        #if DEBUG
        print("received swipe character")
        #endif
        if let personData = data["swipeCharacter"] as? [String: Any] {
            self.swipeCharacter = parsePerson(from: personData)
        }
    }

    private func handleExtraCurriculars(_ data: [String: Any]) {
        if let extracurricularAny = data["extraCurriculars"] {
            if let extracurricularData = extracurricularAny as? [[String: Any]] {
                let jsonData = try? JSONSerialization.data(withJSONObject: extracurricularData)
                let decoder = JSONDecoder()
                if let d = jsonData, let decoded = try? decoder.decode([ExtracurricularClass].self, from: d) {
                    self.extracurriculars = decoded
                }
            }
        }
    }
}
