import Foundation
import SwiftUI
import Combine
import UIKit
import StoreKit

extension Notification.Name {
    static let deviceTokenDidUpdate = Notification.Name("DeviceTokenDidUpdate")
}

// MARK: - WebSocket Configuration
enum WebSocketEnvironment {
    case production
    case development

    var url: String {
        if let overrideURL = ProcessInfo.processInfo.baolifeArgumentValue(named: "--ws-url") ??
            ProcessInfo.processInfo.environment["BAOLIFE_WS_URL"],
           !overrideURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return overrideURL
        }

        switch self {
        case .production:
            return "wss://lichun.app/wss/"
        case .development:
            // Use localhost for iOS Simulator
            return "ws://localhost:8001"
        }
    }
}

// Toggle this to switch between environments
// Set to .development to test with local TypeScript server
let wsEnvironment: WebSocketEnvironment = .production

private extension ProcessInfo {
    func baolifeArgumentValue(named name: String) -> String? {
        guard let index = arguments.firstIndex(of: name),
              arguments.indices.contains(index + 1) else {
            return nil
        }
        return arguments[index + 1]
    }
}

class WebSocketService: ObservableObject {
    // MARK: - Sub-services
    let connection: WebSocketConnection
    let retention: RetentionService = RetentionService()
    let monetization: MonetizationService = MonetizationService()
    let events: EventService = EventService()
    let conversations: ConversationService = ConversationService()
    let gameState: GameStateStore = GameStateStore()
    private var cancellables = Set<AnyCancellable>()
    private var registeredDeviceToken: String?

    var isConnected: Bool { connection.isConnected }
    @Published var appLoaded: Bool = false

    // MARK: - Game State (delegated to GameStateStore)
    var player: Player { gameState.player }
    var person: Person {
        get { gameState.person }
        set { gameState.person = newValue }
    }
    var swipeCharacter: Person? {
        get { gameState.swipeCharacter }
        set { gameState.swipeCharacter = newValue }
    }
    var extracurriculars: [ExtracurricularClass] {
        get { gameState.extracurriculars }
        set { gameState.extracurriculars = newValue }
    }
    var dateIdeas: [DateIdea] {
        get { gameState.dateIdeas }
        set { gameState.dateIdeas = newValue }
    }

    // MARK: - Lifecycle "spine" (delegated to GameStateStore)
    /// Welcome-back digest awaiting a one-time presentation. ContentView reads
    /// this to show OfflineDigestView, then clears it.
    var pendingOfflineDigest: OfflineDigest? {
        get { gameState.pendingOfflineDigest }
        set { gameState.pendingOfflineDigest = newValue }
    }

    /// Begin a new life from the death screen. `mode` selects the legacy path:
    /// "fresh" (clean reset, clears the generational layer) or "heir" (continue
    /// the lineage as the eldest living child; server falls back to fresh when
    /// no heir exists).
    func startNewLife(mode: String) {
        sendMessage(message: WebSocketCommands.startNewLife(mode: mode))
    }

    // MARK: - Events (delegated to EventService)
    var questionQueue: [Question] {
        get { events.questionQueue }
        set { events.questionQueue = newValue }
    }
    var currentQuestion: Question? {
        get { events.currentQuestion }
        set { events.currentQuestion = newValue }
    }
    var currentMessageEvent: MessageEvent? {
        get { events.currentMessageEvent }
        set { events.currentMessageEvent = newValue }
    }
    var currentRelationshipEvent: RelationshipEvent? {
        get { events.currentRelationshipEvent }
        set { events.currentRelationshipEvent = newValue }
    }
    var lifeEvents: [MessageEvent] {
        get { events.lifeEvents }
        set { events.lifeEvents = newValue }
    }
    var unclaimedEventCount: Int { events.unclaimedEventCount }
    // MARK: - Conversations (delegated to ConversationService)
    var awaitingConversationResponse: String {
        get { conversations.awaitingConversationResponse }
        set { conversations.awaitingConversationResponse = newValue }
    }
    var personReceived: Bool {
        get { conversations.personReceived }
        set { conversations.personReceived = newValue }
    }
    var purchaseSuccessful: Bool {
        get { monetization.purchaseSuccessful }
        set { monetization.purchaseSuccessful = newValue }
    }

    // Tutorial state (delegated to EventService)
    var tutorialStep: Int { events.tutorialStep }
    var tutorialComplete: Bool { events.tutorialComplete }
    var tutorialMessage: String? { events.tutorialMessage }
    // Error handling
    @Published var currentError: WebSocketError?

    /// Account-deletion grace-period marker mirrored from `playerObject`'s
    /// top-level `deletionScheduledAt` key. When non-nil (an ISO-8601 string),
    /// the account is scheduled for permanent deletion (now + 30 days). Drives
    /// the "scheduled for deletion" banner and the explicit Cancel affordance.
    /// Cleared when the player cancels (explicitly or by signing back in).
    @Published var deletionScheduledAt: String?

    // MARK: - Monetization (delegated to MonetizationService)
    var energyRefillTiers: [EnergyRefillTier] { monetization.energyRefillTiers }
    @Published var showEnergyRefillModal = false
    var unlimitedEnergyUntil: Date? { monetization.unlimitedEnergyUntil }

    var timeSkipTiers: [TimeSkipTier] { monetization.timeSkipTiers }
    @Published var showTimeSkipModal = false
    var lastTimeSkipSummary: TimeSkipSummary? { monetization.lastTimeSkipSummary }
    @Published var showTimeSkipSummary = false

    // MARK: - Retention (delegated to RetentionService)
    var achievements: [Achievement] { retention.achievements }
    var unacknowledgedAchievements: [Achievement] {
        get { retention.unacknowledgedAchievements }
        set { retention.unacknowledgedAchievements = newValue }
    }
    @Published var showAchievementUnlock = false

    var dailyRewardState: DailyRewardState? { retention.dailyRewardState }
    @Published var showDailyRewards = false

    var dailyQuestsState: DailyQuestsState? { retention.dailyQuestsState }
    @Published var showDailyQuests = false

    /// Per-category achievement collection summary (Wave 2).
    var achievementSummary: AchievementSummary? { retention.achievementSummary }

    var recentAchievementUnlock: Achievement? {
        get { retention.recentAchievementUnlock }
        set { retention.recentAchievementUnlock = newValue }
    }

    var pendingOpenConversation: String? {
        get { conversations.pendingOpenConversation }
        set { conversations.pendingOpenConversation = newValue }
    }

    // MARK: - Error Types
    enum WebSocketError: Identifiable {
        case connectionLost
        case serverError(message: String)
        case insufficientResources(resource: String, required: Int, available: Int)
        case invalidOperation(message: String)
        case timeout

        var id: String {
            switch self {
            case .connectionLost: return "connection_lost"
            case .serverError: return "server_error"
            case .insufficientResources: return "insufficient_resources"
            case .invalidOperation: return "invalid_operation"
            case .timeout: return "timeout"
            }
        }

        var userMessage: String {
            switch self {
            case .connectionLost:
                return "Lost connection to server. Trying to reconnect..."
            case .serverError(let message):
                return message
            case .insufficientResources(let resource, let required, let available):
                return "Not enough \(resource). Need \(required), have \(available)."
            case .invalidOperation(let message):
                return message
            case .timeout:
                return "Request timed out. Please try again."
            }
        }

        var isRetryable: Bool {
            switch self {
            case .connectionLost, .serverError, .timeout:
                return true
            case .insufficientResources, .invalidOperation:
                return false
            }
        }

        var icon: String {
            switch self {
            case .connectionLost:
                return "wifi.slash"
            case .serverError, .timeout:
                return "exclamationmark.triangle.fill"
            case .insufficientResources:
                return "xmark.circle.fill"
            case .invalidOperation:
                return "hand.raised.fill"
            }
        }
    }

    @Published var devicetoken: String?

    init(urlSession: URLSession, delegateQueue: OperationQueue) {
        self.connection = WebSocketConnection(urlSession: urlSession)
        self.appLoaded = true

        // Wire up sub-services
        retention.facade = self
        monetization.facade = self
        events.facade = self
        conversations.facade = self
        gameState.facade = self

        // Forward sub-service changes to trigger SwiftUI updates
        connection.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        connection.$isConnected
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isConnected in
                if isConnected {
                    self?.registerSavedDeviceTokenIfNeeded()
                }
            }
            .store(in: &cancellables)

        retention.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        monetization.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        events.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        conversations.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        gameState.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)

        // Wire up message handling
        connection.onMessage = { [weak self] text in
            self?.handleReceivedMessage(text)
        }

        // Wire up error handling
        connection.onError = { [weak self] in
            DispatchQueue.main.async {
                self?.currentError = .connectionLost
            }
        }

        NotificationCenter.default.publisher(for: .deviceTokenDidUpdate)
            .compactMap { $0.userInfo?["token"] as? String }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] token in
                self?.devicetoken = token
                self?.registeredDeviceToken = nil
                self?.registerSavedDeviceTokenIfNeeded(force: true)
            }
            .store(in: &cancellables)
    }

    func connect() {
        connection.connect()
    }

    func disconnect() {
        connection.disconnect()
    }

    func sendMessage(message: [String: Any]) {
        connection.sendMessage(message: message)
    }

    /// Cancel a scheduled account deletion during the 30-day grace period.
    /// The server clears the persisted marker and replies with
    /// `accountDeletionCancelled`, which clears `deletionScheduledAt` locally.
    func cancelAccountDeletion() {
        connection.sendMessage(message: WebSocketCommands.cancelAccountDeletion())
    }

    func registerSavedDeviceTokenIfNeeded(force: Bool = false) {
        guard connection.isConnected,
              let token = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.deviceToken),
              !token.isEmpty else {
            return
        }

        if !force && registeredDeviceToken == token {
            return
        }

        devicetoken = token
        registeredDeviceToken = token
        connection.sendMessage(message: WebSocketCommands.deviceToken(token))
    }



    // MARK: - Event Methods (delegated)

    func sendEventResponse(eventId: String, choiceId: String) { events.sendEventResponse(eventId: eventId, choiceId: choiceId) }
    func sendAnswer(answer: AnswerOption, questionID: String) { events.sendAnswer(answer: answer, questionID: questionID) }
    func removeQuestion(eventId: String) { events.removeQuestion(eventId: eventId) }
    func claimEvent(_ event: MessageEvent) { events.claimEvent(event) }

    // MARK: - Monetization Methods (delegated)

    func fetchEnergyRefillTiers() { monetization.fetchEnergyRefillTiers() }
    func purchaseEnergyRefill(tierType: String) { monetization.purchaseEnergyRefill(tierType: tierType) }
    func fetchTimeSkipTiers() { monetization.fetchTimeSkipTiers() }
    func purchaseTimeSkip(tierType: String) { monetization.purchaseTimeSkip(tierType: tierType) }

    // MARK: - Retention Methods (delegated)

    func fetchAchievements() { retention.fetchAchievements() }
    func acknowledgeAchievement(_ achievementId: String) { retention.acknowledgeAchievement(achievementId) }
    func fetchDailyRewards() { retention.fetchDailyRewards() }
    func claimDailyReward(day: Int) { retention.claimDailyReward(day: day) }
    func fetchDailyQuests() { retention.fetchDailyQuests() }
    func claimQuestReward(questId: String) { retention.claimQuestReward(questId: questId) }
    func setRecentAchievementUnlock(_ achievement: Achievement) { retention.recentAchievementUnlock = achievement }

    // MARK: - Player-Initiated Activities (Wave 2)

    /// Send a `performActivity` command for one of the catalog activities. The
    /// server applies the stat/energy/money deltas and sends back a refreshed
    /// playerObject plus an `activityPerformed`/`activityPlanned` confirmation.
    func performActivity(activityId: String, override: Bool = false) {
        sendMessage(message: WebSocketCommands.performActivity(activityId: activityId, override: override))
    }

    private func handleReceivedMessage(_ text: String) {
        // Clear connection-related errors when we receive a message
        if case .connectionLost = currentError {
            currentError = nil
        }

        let jsonData = Data(text.utf8)
        do {
            if let parsedJson = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
                if let eventV2Envelope = self.events.decodeEventV2Envelope(from: parsedJson) {
                    self.events.handleEventV2Envelope(eventV2Envelope)
                } else if let type = parsedJson["type"] as? String {
                    if gameState.handleMessage(type: type, data: parsedJson) {
                        // Handled by GameStateStore
                    } else if events.handleMessage(type: type, data: parsedJson) {
                        // Handled by EventService
                    } else if conversations.handleMessage(type: type, data: parsedJson) {
                        // Handled by ConversationService
                    } else if (type == "dataExportComplete" || type == "dataExportReady") {
                        // GDPR: Support canonical backend event (dataExportComplete) and legacy alias.
                        if let exportObj = parsedJson["data"] as? [String: Any],
                           let data = try? JSONSerialization.data(withJSONObject: exportObj, options: [.prettyPrinted]),
                           let exportData = String(data: data, encoding: .utf8) {
                            NotificationCenter.default.post(
                                name: NSNotification.Name("DataExportReady"),
                                object: nil,
                                userInfo: ["exportData": exportData]
                            )
                        } else if let exportData = parsedJson["exportData"] as? String {
                            NotificationCenter.default.post(
                                name: NSNotification.Name("DataExportReady"),
                                object: nil,
                                userInfo: ["exportData": exportData]
                            )
                        } else {
                            let errorMessage = parsedJson["error"] as? String ?? "Unknown error"
                            NotificationCenter.default.post(
                                name: NSNotification.Name("DataExportFailed"),
                                object: nil,
                                userInfo: ["error": errorMessage]
                            )
                        }
                    } else if (type == "accountDeletionScheduled" || type == "accountDeleted") {
                        // GDPR: Support canonical backend event (accountDeletionScheduled) and legacy alias.
                        let success = (parsedJson["result"] as? [String: Any])?["success"] as? Bool
                            ?? parsedJson["success"] as? Bool
                            ?? false
                        if success {
                            let message = parsedJson["message"] as? String
                                ?? "Account deletion scheduled for 30 days"
                            NotificationCenter.default.post(
                                name: NSNotification.Name("AccountDeleted"),
                                object: nil,
                                userInfo: ["message": message]
                            )
                        } else {
                            let errorMessage = parsedJson["error"] as? String ?? "Unknown error"
                            NotificationCenter.default.post(
                                name: NSNotification.Name("AccountDeletionFailed"),
                                object: nil,
                                userInfo: ["error": errorMessage]
                            )
                        }
                    } else if (type == "accountDeletionCancelled") {
                        // Clear local scheduled-deletion state so the banner /
                        // Cancel button disappear immediately, then surface a toast.
                        // Covers both the explicit Cancel button and the
                        // auto-cancel-on-login path (reason == "login").
                        let cancelMessage = parsedJson["message"] as? String ?? "Account deletion cancelled"
                        self.deletionScheduledAt = nil
                        self.player.deletionScheduledAt = nil
                        ToastManager.shared.show(.success, message: cancelMessage)
                        NotificationCenter.default.post(
                            name: NSNotification.Name("AccountDeletionCancelled"),
                            object: nil,
                            userInfo: ["message": cancelMessage]
                        )
                    } else if monetization.handleMessage(type: type, data: parsedJson) {
                        // Handled by MonetizationService
                    } else if retention.handleMessage(type: type, data: parsedJson) {
                        // Handled by RetentionService
                    } else if type == "error", let message = parsedJson["message"] as? String, !message.isEmpty {
                        self.currentError = .serverError(message: message)
                        ToastManager.shared.show(.error, message: message)
                    }

                    if type == "playerObject" {
                        // Mirror the top-level account-deletion marker so the UI
                        // can show a "scheduled for deletion" banner + Cancel.
                        // GameStateStore handles the rest of playerObject; this
                        // field lives here because it drives WebSocketService UI state.
                        let scheduled = parsedJson["deletionScheduledAt"] as? String
                        self.deletionScheduledAt = scheduled
                        self.player.deletionScheduledAt = scheduled
                        registerSavedDeviceTokenIfNeeded()
                    }
                }
            }
        } catch {
            #if DEBUG
            print("Error parsing JSON: \(error)")
            #endif
        }
    }
    // MARK: - Conversation Methods (delegated)

    func appendMessage(_ message: ConversationMessage, to conversation: ConversationObj) { conversations.appendMessage(message, to: conversation) }
    func prefetchConversation(forCharacter characterId: String) { conversations.prefetchConversation(forCharacter: characterId) }
    func clearPendingConversationRequest(forCharacter characterId: String) { conversations.clearPendingConversationRequest(forCharacter: characterId) }
    func openConversation(characterId: String) { conversations.openConversation(characterId: characterId) }
}
