// SPDX-FileCopyrightText: 2026 John Burwell and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

import Foundation

struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
    let device: ConnectedDevice
    let containers: [DeviceAppContainer]
    let matchedSourceID: URL?
    let discoveryErrorDescription: String?

    var id: String { device.id }

    var minecraftContainer: DeviceAppContainer? {
        containers.first(where: { $0.appID == "com.mojang.minecraftpe" })
            ?? containers.first(where: { $0.minecraftFolderRelativePath != nil })
    }

    var hasMinecraftContainer: Bool {
        minecraftContainer != nil
    }
}

struct CachedConnectedDeviceDiscovery {
    let device: ConnectedDevice
    let containers: [DeviceAppContainer]
    let discoveryErrorDescription: String?
    let refreshedAt: Date
}

struct ConnectedDeviceRefreshContext {
    let existingSources: [MinecraftSource]
    let sourceFactory: ConnectedDeviceSourceFactory

    func knownSourceIDs(for device: ConnectedDevice) -> [URL] {
        existingSources.compactMap { source -> URL? in
            guard case .connectedDevice(let expectedDevice, _) = source.origin else {
                return nil
            }

            return expectedDevice.udid == device.udid ? source.id : nil
        }
    }

    func hasKnownSource(for device: ConnectedDevice) -> Bool {
        !knownSourceIDs(for: device).isEmpty
    }

    func matchingSourceID(
        for device: ConnectedDevice,
        containers: [DeviceAppContainer]
    ) -> URL? {
        for container in containers {
            let sourceID = sourceFactory.makeSourceIdentifier(
                device: device,
                container: container
            )
            if existingSources.contains(where: { $0.id == sourceID }) {
                return sourceID
            }
        }

        return nil
    }
}

enum ConnectedDeviceDiscoveryCachePolicy {
    private static let usbCacheTTL: TimeInterval = 60.0
    private static let networkCacheTTL: TimeInterval = 180.0

    static func cachedDiscovery(
        for device: ConnectedDevice,
        cache: [String: CachedConnectedDeviceDiscovery],
        isActivelyScanning: Bool,
        now: Date = Date()
    ) -> CachedConnectedDeviceDiscovery? {
        guard let cachedDiscovery = cache[device.udid] else {
            return nil
        }

        if isActivelyScanning {
            return cachedDiscovery
        }

        let age = now.timeIntervalSince(cachedDiscovery.refreshedAt)
        guard age <= discoveryCacheTTL(for: device) else {
            return nil
        }

        guard cachedDiscovery.device.connection == device.connection,
              cachedDiscovery.device.trustState == device.trustState,
              cachedDiscovery.device.name == device.name else {
            return nil
        }

        return cachedDiscovery
    }

    static func cacheDiscovery(
        for device: ConnectedDevice,
        containers: [DeviceAppContainer],
        discoveryErrorDescription: String?,
        now: Date = Date()
    ) -> CachedConnectedDeviceDiscovery {
        CachedConnectedDeviceDiscovery(
            device: device,
            containers: containers,
            discoveryErrorDescription: discoveryErrorDescription,
            refreshedAt: now
        )
    }

    private static func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval {
        switch device.connection {
        case .usb:
            return usbCacheTTL
        case .network:
            return networkCacheTTL
        }
    }
}

enum ConnectedDeviceSourcePolicy {
    static func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability {
        guard hasMinecraftContainer else {
            return .unavailable
        }

        switch device.trustState {
        case .trusted:
            return .available
        case .locked, .untrusted:
            return .limited
        case .unavailable:
            return .disconnected
        }
    }

    static func preferredDeviceName(
        currentName: String,
        fallbackDeviceName: String,
        fallbackDisplayName: String
    ) -> String {
        if let sanitizedCurrentName = sanitizedDeviceName(currentName) {
            return sanitizedCurrentName
        }

        if let sanitizedFallbackName = sanitizedDeviceName(fallbackDeviceName) {
            return sanitizedFallbackName
        }

        if let displayNamePrefix = fallbackDisplayName.components(separatedBy: " • ").first,
           let sanitizedDisplayName = sanitizedDeviceName(displayNamePrefix) {
            return sanitizedDisplayName
        }

        return "Unknown Device"
    }

    static func sanitizedDeviceName(_ candidate: String) -> String? {
        let trimmedCandidate = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedCandidate.isEmpty else {
            return nil
        }

        let normalizedCandidate = trimmedCandidate.lowercased()
        guard normalizedCandidate != "unknown device",
              normalizedCandidate != "unknown device..." else {
            return nil
        }

        return trimmedCandidate
    }

    static func shouldRefreshSource(_ source: MinecraftSource) -> Bool {
        guard !source.isScanning else {
            return false
        }

        return hasRefreshDebt(source)
    }

    static func shouldRefreshSourceOnReconnect(_ source: MinecraftSource, now: Date = Date()) -> Bool {
        guard !source.isScanning else {
            return false
        }

        if hasRefreshDebt(source) {
            return true
        }

        guard let lastScanDate = source.lastScanDate else {
            return true
        }

        let reconnectGracePeriod: TimeInterval = 5 * 60
        if now.timeIntervalSince(lastScanDate) < reconnectGracePeriod {
            return false
        }

        return shouldRefreshSource(source)
    }

    static func hasRefreshDebt(_ source: MinecraftSource) -> Bool {
        guard source.origin.kind == .connectedDevice else {
            return false
        }

        guard !source.rawItems.isEmpty else {
            return true
        }

        let itemCount = source.rawItems.count
        return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
    }
}
