//
//  Conversation.swift
//  lichunWebsocket
//
//  Conversation-related models
//

import Foundation

// MARK: - Conversation Class
struct ConversationClass: Identifiable, Decodable, Encodable, Hashable {
    var id: UUID = UUID()
    var fname: String
    var button: String
}

// MARK: - Message Status
enum MessageStatus: String, Codable {
    case pending    // Sent but not confirmed by server
    case sent       // Confirmed by server
    case failed     // Timeout or error
}

// MARK: - Conversation Message
struct ConversationMessage: Codable, Hashable, Identifiable {
    let id: String
    let message: String
    let sentiment: String
    let affinityDelta: Int?
    var answerOptions: [String]?
    let sender: String?
    let datetime: String
    let date: String
    let time: String

    // New properties for message status tracking
    var status: MessageStatus
    var tempId: String?          // Client-generated ID for reconciliation
    var failureReason: String?   // Error context for failed messages

    func hash(into hasher: inout Hasher) {
        hasher.combine(datetime)
        hasher.combine(tempId)  // Include tempId in hash for uniqueness
    }

    enum CodingKeys: String, CodingKey {
        case id
        case message
        case answerOptions
        case sender
        case sentiment
        case affinityDelta
        case date
        case time
        case datetime
        case status
        case tempId
        case failureReason
    }

    static func ==(lhs: ConversationMessage, rhs: ConversationMessage) -> Bool {
        // If both have tempIds, compare by tempId for better matching
        if let lhsTempId = lhs.tempId, let rhsTempId = rhs.tempId {
            return lhsTempId == rhsTempId
        }
        return lhs.id == rhs.id
    }

    /// Check if this message is from the current player
    func isFromCurrentUser(playerCharacterId: String) -> Bool {
        if isSystemMessage { return false }

        // Message is from player if sender is set and does NOT match the NPC character
        // Server sets sender to the actual character ID (player or NPC)
        guard let sender = sender else { return false }
        return sender != playerCharacterId && sender != ""
    }

    /// Server-authored delivery/status notices should not render as player bubbles.
    var isSystemMessage: Bool {
        sender == "system"
    }

    /// Create a pending message for optimistic UI
    static func pending(message: String, senderId: String, tempId: String) -> ConversationMessage {
        let now = Date()
        let isoFormatter = ISO8601DateFormatter()
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "MM-dd"
        let timeFormatter = DateFormatter()
        timeFormatter.dateFormat = "HH:mm"

        return ConversationMessage(
            id: tempId,  // Use tempId as id until server confirms
            message: message,
            sentiment: "neutral",
            affinityDelta: nil,
            answerOptions: nil,
            sender: senderId,
            datetime: isoFormatter.string(from: now),
            date: dateFormatter.string(from: now),
            time: timeFormatter.string(from: now),
            status: .pending,
            tempId: tempId,
            failureReason: nil
        )
    }

    init(id: String, message: String, sentiment: String, affinityDelta: Int? = nil, answerOptions: [String]? = nil, sender: String? = nil, datetime: String, date: String, time: String, status: MessageStatus = .sent, tempId: String? = nil, failureReason: String? = nil) {
        self.id = id
        self.message = message
        self.sentiment = sentiment
        self.affinityDelta = affinityDelta
        self.answerOptions = answerOptions
        self.sender = sender
        self.datetime = datetime
        self.date = date
        self.time = time
        self.status = status
        self.tempId = tempId
        self.failureReason = failureReason
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        message = try container.decode(String.self, forKey: .message)
        answerOptions = try container.decodeIfPresent([String].self, forKey: .answerOptions)
        sender = try container.decodeIfPresent(String.self, forKey: .sender)

        // Custom decoding for sentiment to handle "<null>" as nil
        let sentimentValue = try container.decodeIfPresent(String.self, forKey: .sentiment)
        sentiment = ((sentimentValue != nil && sentimentValue != "<null>") ? sentimentValue : nil) ?? ""

        // Decode affinityDelta (optional integer, -50 to +30 range from backend)
        affinityDelta = try container.decodeIfPresent(Int.self, forKey: .affinityDelta)

        // Decode datetime, date, and time
        // datetime is required, but date and time may be missing in some server messages
        datetime = try container.decode(String.self, forKey: .datetime)

        // Fallback: if date/time not provided, extract from datetime (ISO8601 format)
        if let dateValue = try container.decodeIfPresent(String.self, forKey: .date) {
            date = dateValue
        } else {
            // Extract date from datetime (e.g., "2024-01-15T10:30:00Z" -> "01-15")
            let components = datetime.prefix(10).split(separator: "-")
            if components.count >= 3 {
                date = "\(components[1])-\(components[2])"
            } else {
                date = ""
            }
        }

        if let timeValue = try container.decodeIfPresent(String.self, forKey: .time) {
            time = timeValue
        } else {
            // Extract time from datetime (e.g., "2024-01-15T10:30:00Z" -> "10:30")
            if datetime.count > 11 {
                let timeStart = datetime.index(datetime.startIndex, offsetBy: 11)
                let timeEnd = datetime.index(timeStart, offsetBy: min(5, datetime.distance(from: timeStart, to: datetime.endIndex)))
                time = String(datetime[timeStart..<timeEnd])
            } else {
                time = ""
            }
        }

        // Decode new status fields with defaults for backward compatibility
        status = try container.decodeIfPresent(MessageStatus.self, forKey: .status) ?? .sent
        tempId = try container.decodeIfPresent(String.self, forKey: .tempId)
        failureReason = try container.decodeIfPresent(String.self, forKey: .failureReason)
    }
}

// MARK: - Conversation Object
struct ConversationObj: Codable, Hashable, Identifiable {
    var id: String
    var type = "conversationEvent"
    let cType: String?
    let character: String?
    var conversation: [ConversationMessage]
    var question: Int
    var unread: Bool = false  // Backend may send as Bool or Int (1/0)

    enum CodingKeys: String, CodingKey {
        case id, type, cType, character, conversation, question, unread
    }

    init(id: String, cType: String? = nil, character: String? = nil, conversation: [ConversationMessage] = [], question: Int = 0, unread: Bool = false) {
        self.id = id
        self.cType = cType
        self.character = character
        self.conversation = conversation
        self.question = question
        self.unread = unread
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        type = try container.decodeIfPresent(String.self, forKey: .type) ?? "conversationEvent"
        cType = try container.decodeIfPresent(String.self, forKey: .cType)
        character = try container.decodeIfPresent(String.self, forKey: .character)
        conversation = try container.decodeIfPresent([ConversationMessage].self, forKey: .conversation) ?? []
        question = try container.decodeIfPresent(Int.self, forKey: .question) ?? 0

        // Handle unread as Bool or Int (backend may send 1/0 from database)
        if let boolValue = try? container.decode(Bool.self, forKey: .unread) {
            unread = boolValue
        } else if let intValue = try? container.decode(Int.self, forKey: .unread) {
            unread = intValue != 0
        } else {
            unread = false
        }
    }

    /// Parse datetime string that may be in ISO8601 or custom format
    private static func parseDateTime(_ dateString: String) -> Date? {
        // Try ISO8601 format first (server sends this)
        let iso8601Formatter = ISO8601DateFormatter()
        iso8601Formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = iso8601Formatter.date(from: dateString) {
            return date
        }

        // Try ISO8601 without fractional seconds
        iso8601Formatter.formatOptions = [.withInternetDateTime]
        if let date = iso8601Formatter.date(from: dateString) {
            return date
        }

        // Fallback to custom format (legacy)
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"
        return dateFormatter.date(from: dateString)
    }

    /// Get the most recent message by datetime (not just array position)
    var mostRecentMessage: ConversationMessage? {
        guard !conversation.isEmpty else { return nil }

        return conversation.max { m1, m2 in
            guard let date1 = ConversationObj.parseDateTime(m1.datetime),
                  let date2 = ConversationObj.parseDateTime(m2.datetime) else {
                return false
            }
            return date1 < date2
        }
    }

    /// Messages sorted chronologically (oldest first for chat display)
    var sortedMessages: [ConversationMessage] {
        return conversation.sorted { m1, m2 in
            guard let date1 = ConversationObj.parseDateTime(m1.datetime),
                  let date2 = ConversationObj.parseDateTime(m2.datetime) else {
                return false
            }
            return date1 < date2
        }
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(type)
        hasher.combine(cType)
        hasher.combine(character)
        hasher.combine(question)
    }

    static func ==(lhs: ConversationObj, rhs: ConversationObj) -> Bool {
        return lhs.type == rhs.type && lhs.cType == rhs.cType && lhs.character == rhs.character && lhs.question == rhs.question
    }
}
