import Combine
import Foundation

/// Entspricht `CommandTableState` in `ChatRepository.kt`.
struct CommandTableState: Equatable, Sendable {
    var title: String
    var columns: [String]
    var rows: [[String]]
}

/// Zentraler UI-Zustand wie `ChatState` in `ChatRepository.kt`.
struct ChatState: Equatable, Sendable {
    var isConnected: Bool = false
    var isLoggedIn: Bool = false
    var expressSessionId: String?
    var currentUser: UserDto?
    var users: [UserDto] = []
    var currentConversation: String?
    var messages: [ChatMessageDto] = []
    var searchResults: [UserDto] = []
    var inboxResults: [InboxItemDto] = []
    var historyResults: [HistoryItemDto] = []
    var countries: [CountryOption] = []
    var feedbackItems: [FeedbackItemDto] = []
    var feedbackMessage: String?
    var feedbackAdminAuthenticated: Bool = false
    var feedbackAdminUserName: String?
    var feedbackAdminError: String?
    var partnerLinks: [PartnerLinkDto] = []
    var partnersError: String?
    var savedProfile: SavedProfile = SavedProfile()
    var commandLines: [String] = []
    var commandKind: String?
    var commandTable: CommandTableState?
    var awaitingLoginUsername: Bool = false
    var awaitingLoginPassword: Bool = false
    var remainingSecondsToTimeout: Int = 1800
    var isUploadingImage: Bool = false
    var imageUploadMessage: String?
    var unreadChatsCount: Int = 0
    var errorMessage: String?
}

/// Analog `ChatRepository.kt`: REST + Socket + `reduce` + Timeout.
final class ChatRepository: ObservableObject {
    @Published private(set) var state = ChatState()

    private let api: RestAPIClient
    private let socket: SocketClient
    private let profileStore: ProfileStore

    private var cancellables = Set<AnyCancellable>()
    private var timeoutTickerStarted = false

    init(api: RestAPIClient, socket: SocketClient, profileStore: ProfileStore) {
        self.api = api
        self.socket = socket
        self.profileStore = profileStore

        socket.eventPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] event in
                self?.reduce(event)
            }
            .store(in: &cancellables)

        startTimeoutTicker()
    }

    // MARK: - Session / Profil

    func restoreSession() async {
        var s = state
        s.savedProfile = profileStore.read()
        state = s

        await loadCountries()
        await loadFeedbackAdminStatus()

        do {
            let session = try await api.sessionStatus()
            var next = state
            next.expressSessionId = session.sessionId
            next.isLoggedIn = session.loggedIn && session.user != nil
            next.currentUser = session.user
            next.errorMessage = nil
            state = next
            if session.loggedIn, session.user != nil {
                resetTimeout()
            }
            connectSocket(expressSessionId: session.sessionId)
        } catch {
            var next = state
            next.errorMessage = error.localizedDescription
            state = next
        }
    }

    func loadCountries() async {
        do {
            let countries = try await api.countries()
            let locale = Locale.current
            let options: [CountryOption] = countries.map { englishName, code in
                let normalized = code.uppercased()
                let localized = Locale.current.localizedString(forRegionCode: normalized)
                let display = localized.flatMap { $0.isEmpty ? nil : $0 } ?? englishName
                return CountryOption(englishName: englishName, displayName: display, isoCode: code)
            }
            .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }

            var next = state
            next.countries = options
            state = next
        } catch {
            var next = state
            next.errorMessage = "Country list could not be loaded: \(error.localizedDescription)"
            state = next
        }
    }

    func login(userName: String, gender: String, age: Int, country: String) async {
        profileStore.write(SavedProfile(nickname: userName, gender: gender, age: age, country: country))

        let session = try? await api.sessionStatus()
        let sessionId = session?.sessionId ?? state.expressSessionId

        var next = state
        next.expressSessionId = sessionId
        state = next

        if !socket.isConnected {
            connectSocket(expressSessionId: sessionId)
        }
        socket.login(userName: userName, gender: gender, age: age, country: country, expressSessionId: sessionId)
        resetTimeout()
    }

    func logout() async {
        try? await api.logout()
        socket.disconnect()
        clearCookies()
        var fresh = ChatState()
        fresh.savedProfile = profileStore.read()
        fresh.countries = state.countries
        state = fresh
    }

    func connectSocket(expressSessionId: String? = nil) {
        let sid = expressSessionId ?? state.expressSessionId
        socket.connect()
        if let sid {
            socket.setSessionId(sid)
        }
    }

    // MARK: - Chat

    func openConversation(userName: String) {
        var next = state
        next.currentConversation = userName
        next.messages = []
        state = next
        socket.requestConversation(withUserName: userName)
        resetTimeout()
    }

    func closeConversation() {
        var next = state
        next.currentConversation = nil
        next.messages = []
        state = next
    }

    func sendMessage(text: String) {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        let target = state.currentConversation
        let isCommand = trimmed.hasPrefix("/")
        if target == nil, !isCommand { return }

        socket.sendMessage(toUserName: target, message: trimmed)
        if !isCommand {
            let msg = ChatMessageDto(
                from: state.currentUser?.userName ?? "",
                to: target,
                message: trimmed,
                messageId: nil,
                timestamp: ISO8601DateFormatter().string(from: Date()),
                read: false,
                isImage: false,
                imageType: nil,
                imageUrl: nil,
                imageCode: nil
            )
            var next = state
            next.messages.append(msg)
            state = next
        }
        resetTimeout()
    }

    func sendImage(toUserName: String, imageCode: String, imageUrl: String) {
        let absoluteUrl: String =
            imageUrl.hasPrefix("http") ? imageUrl : AppConfig.baseURL + imageUrl
        socket.sendImage(toUserName: toUserName, imageCode: imageCode, imageUrl: absoluteUrl)
        let msg = ChatMessageDto(
            from: state.currentUser?.userName ?? "",
            to: toUserName,
            message: absoluteUrl,
            messageId: nil,
            timestamp: ISO8601DateFormatter().string(from: Date()),
            read: false,
            isImage: true,
            imageType: nil,
            imageUrl: absoluteUrl,
            imageCode: imageCode
        )
        var next = state
        next.messages.append(msg)
        state = next
        resetTimeout()
    }

    func setImageUploadState(inProgress: Bool, message: String? = nil) {
        var next = state
        next.isUploadingImage = inProgress
        next.imageUploadMessage = message
        state = next
    }

    func uploadImage(data: Data, fileName: String, mimeType: String) async throws -> ImageUploadResponse {
        let result = try await api.uploadImage(data: data, fileName: fileName, mimeType: mimeType)
        return result.response
    }

    // MARK: - Feedback / Partner

    func loadFeedback() async {
        do {
            let response = try await api.feedback()
            var next = state
            next.feedbackItems = response.items
            next.feedbackMessage = nil
            state = next
        } catch {
            var next = state
            next.feedbackMessage = error.localizedDescription
            state = next
        }
    }

    func submitFeedback(comment: String) async {
        let trimmed = comment.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        let profile = state.savedProfile
        do {
            try await api.submitFeedback(
                FeedbackRequest(
                    name: profile.nickname,
                    age: profile.age,
                    country: profile.country,
                    gender: profile.gender,
                    comment: trimmed
                )
            )
            var next = state
            next.feedbackMessage = "Feedback saved"
            state = next
            await loadFeedback()
        } catch {
            var next = state
            next.feedbackMessage = error.localizedDescription
            state = next
        }
    }

    func loadFeedbackAdminStatus() async {
        do {
            let response = try await api.feedbackAdminStatus()
            var next = state
            next.feedbackAdminAuthenticated = response.authenticated
            next.feedbackAdminUserName = response.username
            next.feedbackAdminError = nil
            state = next
        } catch {
            var next = state
            next.feedbackAdminAuthenticated = false
            next.feedbackAdminUserName = nil
            state = next
        }
    }

    func loginFeedbackAdmin(username: String, password: String) async {
        do {
            let response = try await api.feedbackAdminLogin(FeedbackAdminLoginRequest(username: username, password: password))
            var next = state
            next.feedbackAdminAuthenticated = true
            next.feedbackAdminUserName = response.username
            next.feedbackAdminError = nil
            state = next
            await loadFeedback()
        } catch {
            var next = state
            next.feedbackAdminError = error.localizedDescription
            state = next
        }
    }

    func logoutFeedbackAdmin() async {
        try? await api.feedbackAdminLogout()
        var next = state
        next.feedbackAdminAuthenticated = false
        next.feedbackAdminUserName = nil
        next.feedbackAdminError = nil
        state = next
    }

    func deleteFeedback(id: String) async {
        do {
            try await api.deleteFeedback(id: id)
            await loadFeedback()
        } catch {
            var next = state
            next.feedbackAdminError = error.localizedDescription
            state = next
        }
    }

    func loadPartners() async {
        do {
            let links = try await api.partners()
            var next = state
            next.partnerLinks = links
            next.partnersError = nil
            state = next
        } catch {
            var next = state
            next.partnersError = error.localizedDescription
            state = next
        }
    }

    // MARK: - Socket actions

    func search(nameIncludes: String?, minAge: Int?, maxAge: Int?, countries: [String], genders: [String]) {
        socket.userSearch(nameIncludes: nameIncludes, minAge: minAge, maxAge: maxAge, countries: countries, genders: genders)
        resetTimeout()
    }

    func requestInbox() {
        socket.requestOpenConversations()
        resetTimeout()
    }

    func requestHistory() {
        socket.requestHistory()
        resetTimeout()
    }

    func blockUser(userName: String) {
        socket.blockUser(userName: userName)
        resetTimeout()
    }

    func unblockUser(userName: String) {
        socket.unblockUser(userName: userName)
        resetTimeout()
    }

    // MARK: - Private

    private func clearCookies() {
        HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) }
    }

    private func startTimeoutTicker() {
        guard !timeoutTickerStarted else { return }
        timeoutTickerStarted = true
        Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                self?.runTimeoutTick()
            }
            .store(in: &cancellables)
    }

    private func runTimeoutTick() {
        guard state.isLoggedIn else { return }
        let nextSec = max(0, state.remainingSecondsToTimeout - 1)
        var next = state
        next.remainingSecondsToTimeout = nextSec
        state = next
        if nextSec == 0 {
            Task { await self.logout() }
        }
    }

    private func resetTimeout() {
        guard state.isLoggedIn || state.currentUser != nil else { return }
        var next = state
        next.remainingSecondsToTimeout = 1800
        state = next
    }

    private func reduce(_ event: SocketEvent) {
        state = Self.apply(state: state, event: event, socket: socket)
    }

    private static func apply(state: ChatState, event: SocketEvent, socket: SocketClient) -> ChatState {
        var current = state
        switch event {
        case .connectionChanged(let connected, _):
            current.isConnected = connected

        case .connected(let sessionId, let loggedIn, let user):
            if let sid = sessionId {
                socket.setSessionId(sid)
            }
            current.expressSessionId = sessionId ?? current.expressSessionId
            current.isLoggedIn = loggedIn || current.isLoggedIn
            current.currentUser = user ?? current.currentUser
            current.errorMessage = nil
            if current.isLoggedIn {
                current.remainingSecondsToTimeout = 1800
            }

        case .loginSuccess(let sessionId, let user):
            current.expressSessionId = sessionId ?? current.expressSessionId
            current.isLoggedIn = true
            current.currentUser = user
            current.errorMessage = nil
            current.remainingSecondsToTimeout = 1800

        case .userList(let users):
            current.users = users

        case .incomingMessage(let message):
            let active = current.currentConversation == message.from
            if active {
                current.messages.append(message)
            }
            if !active {
                current.unreadChatsCount += 1
            }
            current.remainingSecondsToTimeout = 1800

        case .messageSent:
            break

        case .conversation(let withUserName, let messages):
            current.currentConversation = withUserName
            current.messages = messages
            current.unreadChatsCount = max(0, current.unreadChatsCount - 1)
            current.remainingSecondsToTimeout = 1800

        case .searchResults(let results):
            current.searchResults = results

        case .historyResults(let results):
            current.historyResults = results

        case .inboxResults(let results):
            current.inboxResults = results

        case .unreadChats(let count):
            current.unreadChatsCount = count

        case .userBlocked(let userName):
            current.errorMessage = "\(userName) blocked"

        case .userUnblocked(let userName):
            current.errorMessage = "\(userName) unblocked"

        case .commandResult(let lines, let kind):
            current.commandLines = lines
            current.commandKind = kind
            current.commandTable = nil
            current.awaitingLoginUsername = kind == "loginPromptUsername"
            current.awaitingLoginPassword = kind == "loginPromptPassword"
            if kind == "info" || kind.hasPrefix("login") {
                current.errorMessage = lines.joined(separator: " | ")
            }

        case .commandTable(let title, let columns, let rows):
            current.commandLines = []
            current.commandKind = nil
            current.commandTable = CommandTableState(title: title, columns: columns, rows: rows)

        case .error(let message):
            current.errorMessage = message
        }
        return current
    }
}
