//
//  InstancesManager.swift
//  Yattee
//
//  Manages configured backend instances with iCloud sync.
//

import Foundation
import SwiftUI

/// Status of an instance's connectivity and authentication.
enum InstanceStatus: Equatable {
    /// Instance is online and working.
    case online
    /// Instance is offline or unreachable.
    case offline
    /// Instance requires authentication but credentials are not provided.
    case authRequired
    /// Instance authentication failed (wrong credentials).
    case authFailed
}

/// Manages the list of configured backend instances with iCloud sync.
@MainActor
@Observable
final class InstancesManager {
    // MARK: - Storage

    private let localDefaults = UserDefaults.standard
    private let ubiquitousStore = NSUbiquitousKeyValueStore.default
    private let instancesKey = "configuredInstances"
    private let activeInstanceKey = "activeInstanceID"

    // MARK: - Dependencies

    private weak var settingsManager: SettingsManager?

    // MARK: - State

    private(set) var instances: [Instance] = []
    private(set) var activeInstanceID: UUID?

    /// Current status of each instance, keyed by instance ID.
    private(set) var instanceStatuses: [UUID: InstanceStatus] = [:]

    // MARK: - Initialization

    init(settingsManager: SettingsManager? = nil) {
        self.settingsManager = settingsManager

        loadInstances()
        loadActiveInstance()

        // Listen for external changes from iCloud
        NotificationCenter.default.addObserver(
            forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
            object: ubiquitousStore,
            queue: .main
        ) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.handleiCloudChange()
            }
        }
    }

    /// Sets the settings manager reference for checking iCloud sync status.
    func setSettingsManager(_ manager: SettingsManager) {
        self.settingsManager = manager
    }

    /// Whether iCloud sync is currently enabled.
    private var iCloudSyncEnabled: Bool {
        settingsManager?.iCloudSyncEnabled ?? false
    }

    /// Whether instance sync is enabled (requires both master toggle and category toggle).
    private var instanceSyncEnabled: Bool {
        iCloudSyncEnabled && (settingsManager?.syncInstances ?? true)
    }

    /// Handles external iCloud changes by replacing local data with iCloud data.
    private func handleiCloudChange() {
        // Only process iCloud changes if instance sync is enabled
        guard instanceSyncEnabled else { return }

        guard let iCloudData = ubiquitousStore.data(forKey: instancesKey),
              let iCloudInstances = try? JSONDecoder().decode([Instance].self, from: iCloudData) else {
            return
        }

        // Replace local instances with iCloud data
        instances = iCloudInstances
        // Save to local defaults for offline access
        localDefaults.set(iCloudData, forKey: instancesKey)

        // Update sync time
        settingsManager?.updateLastSyncTime()
    }

    /// Syncs local data to iCloud (called when enabling iCloud sync).
    /// Only syncs if instance sync is enabled.
    func syncToiCloud() {
        guard instanceSyncEnabled else { return }

        guard let data = try? JSONEncoder().encode(instances) else { return }
        ubiquitousStore.set(data, forKey: instancesKey)
        ubiquitousStore.synchronize()
        settingsManager?.updateLastSyncTime()
    }

    /// Replaces local data with iCloud data (called when enabling iCloud sync).
    /// Only replaces if instance sync is enabled.
    func replaceWithiCloudData() {
        guard instanceSyncEnabled else { return }

        ubiquitousStore.synchronize()

        guard let iCloudData = ubiquitousStore.data(forKey: instancesKey),
              let iCloudInstances = try? JSONDecoder().decode([Instance].self, from: iCloudData) else {
            // No iCloud data exists, sync local data to iCloud
            syncToiCloud()
            return
        }

        // Replace local with iCloud data
        instances = iCloudInstances
        localDefaults.set(iCloudData, forKey: instancesKey)
        settingsManager?.updateLastSyncTime()
    }

    // MARK: - Public Methods

    func add(_ instance: Instance) {
        instances.append(instance)
        saveInstances()
    }

    func remove(_ instance: Instance) {
        instances.removeAll { $0.id == instance.id }

        // Clear active instance if it was the removed one
        if activeInstanceID == instance.id {
            activeInstanceID = nil
            localDefaults.removeObject(forKey: activeInstanceKey)
        }

        saveInstances()
        Task { await ProxyDetectionCache.shared.invalidate(instance: instance) }
    }

    func update(_ instance: Instance) {
        if let index = instances.firstIndex(where: { $0.id == instance.id }) {
            instances[index] = instance
            saveInstances()
            // Editing a source can change the proxy answer (URL change, toggle
            // flip). Drop the cached auto-detect verdict so the next playback
            // re-probes.
            Task { await ProxyDetectionCache.shared.invalidate(instance: instance) }
        }
    }

    /// Alias for add method to maintain consistency.
    func addInstance(_ instance: Instance) {
        add(instance)
    }

    /// Toggles the enabled state of an instance.
    func toggleEnabled(_ instance: Instance) {
        if let index = instances.firstIndex(where: { $0.id == instance.id }) {
            var updated = instances[index]
            updated.isEnabled.toggle()
            instances[index] = updated
            saveInstances()
        }
    }

    /// Records whether an instance sits behind HTTP Basic Auth.
    /// Persisted (and synced to iCloud) so missing Keychain credentials can be
    /// detected after a reinstall.
    func setUsesBasicAuth(_ value: Bool, for instance: Instance) {
        guard let index = instances.firstIndex(where: { $0.id == instance.id }),
              instances[index].usesBasicAuth != value else { return }
        instances[index].usesBasicAuth = value
        saveInstances()
    }

    /// Records whether the user has logged into an account on an instance.
    /// Persisted (and synced to iCloud) so missing Keychain credentials can be
    /// detected after a reinstall.
    func setUsesAccountLogin(_ value: Bool, for instance: Instance) {
        guard let index = instances.firstIndex(where: { $0.id == instance.id }),
              instances[index].usesAccountLogin != value else { return }
        instances[index].usesAccountLogin = value
        saveInstances()
    }

    /// Sets the given instance as the primary (first) instance.
    func setPrimary(_ instance: Instance) {
        LoggingService.shared.debug("[InstancesManager] setPrimary called for: \(instance.displayName)", category: .general)
        LoggingService.shared.debug("[InstancesManager] Current instances: \(instances.map { $0.displayName })", category: .general)

        guard let index = instances.firstIndex(where: { $0.id == instance.id }) else {
            LoggingService.shared.debug("[InstancesManager] Instance not found in list", category: .general)
            return
        }

        if index == 0 {
            LoggingService.shared.debug("[InstancesManager] Instance already at index 0, skipping", category: .general)
            return
        }

        LoggingService.shared.debug("[InstancesManager] Moving instance from index \(index) to 0", category: .general)
        // Move to front
        let removed = instances.remove(at: index)
        instances.insert(removed, at: 0)
        saveInstances()
        LoggingService.shared.debug("[InstancesManager] After move: \(instances.map { $0.displayName })", category: .general)
    }

    // MARK: - Computed Properties

    var enabledInstances: [Instance] {
        instances.filter(\.isEnabled)
    }

    var youtubeInstances: [Instance] {
        instances.filter(\.isYouTubeInstance)
    }

    var peertubeInstances: [Instance] {
        instances.filter(\.isPeerTubeInstance)
    }

    var yatteeServerInstances: [Instance] {
        instances.filter(\.isYatteeServerInstance)
    }

    var hasYouTubeInstances: Bool {
        instances.contains { $0.isYouTubeInstance }
    }

    var hasPeerTubeInstances: Bool {
        instances.contains { $0.isPeerTubeInstance }
    }

    var hasYatteeServerInstances: Bool {
        instances.contains { $0.isYatteeServerInstance }
    }

    var invidiousPipedInstances: [Instance] {
        instances.filter { $0.type == .invidious || $0.type == .piped }
    }

    var hasInvidiousPipedInstances: Bool {
        instances.contains { $0.type == .invidious || $0.type == .piped }
    }

    var enabledYatteeServerInstances: [Instance] {
        yatteeServerInstances.filter(\.isEnabled)
    }

    /// Selects an enabled instance appropriate for the given video's content source.
    /// - For PeerTube videos: prefers the exact instance, falls back to any PeerTube instance
    /// - For YouTube/extracted content: uses YouTube-capable instance (Invidious, Piped, Yattee Server)
    func instance(for video: Video) -> Instance? {
        instance(for: video.id.source)
    }

    /// Selects an enabled instance appropriate for the given content source.
    /// - For PeerTube content: prefers the exact instance, falls back to any PeerTube instance
    /// - For extracted content: requires Yattee Server (only backend with yt-dlp)
    /// - For YouTube content: uses YouTube-capable instance (Invidious, Piped, Yattee Server)
    func instance(for contentSource: ContentSource) -> Instance? {
        switch contentSource {
        case .federated(let provider, let instanceURL) where provider == ContentSource.peertubeProvider:
            // PeerTube content - prefer the exact instance, fall back to any PeerTube instance
            return enabledInstances.first { $0.url.host == instanceURL.host }
                ?? enabledInstances.first(where: \.isPeerTubeInstance)
        case .extracted:
            // Extracted content requires Yattee Server (yt-dlp)
            return yatteeServerInstances.first
        case .global, .federated:
            // YouTube content - prefer Yattee Server, fall back to other YouTube-capable instances
            return enabledYatteeServerInstances.first
                ?? enabledInstances.first(where: \.isYouTubeInstance)
        }
    }

    /// Disables all Yattee Server instances except the specified one.
    func disableOtherYatteeServerInstances(except instanceID: UUID) {
        for instance in enabledYatteeServerInstances where instance.id != instanceID {
            var updated = instance
            updated.isEnabled = false
            update(updated)
        }
    }

    // MARK: - Instance Status

    /// Returns the current status of an instance.
    func status(for instance: Instance) -> InstanceStatus {
        instanceStatuses[instance.id] ?? .online
    }

    /// Updates the status of an instance.
    func updateStatus(_ status: InstanceStatus, for instance: Instance) {
        instanceStatuses[instance.id] = status
    }

    /// Updates status based on an API error.
    func updateStatusFromError(_ error: Error, for instance: Instance) {
        if let apiError = error as? APIError {
            switch apiError {
            case .unauthorized:
                updateStatus(.authFailed, for: instance)
            case .noConnection, .timeout:
                updateStatus(.offline, for: instance)
            default:
                // Don't change status for other errors
                break
            }
        } else {
            // Generic network errors
            let nsError = error as NSError
            if nsError.domain == NSURLErrorDomain {
                updateStatus(.offline, for: instance)
            }
        }
    }

    /// Clears the status of an instance (resets to online).
    func clearStatus(for instance: Instance) {
        instanceStatuses.removeValue(forKey: instance.id)
    }

    /// Instances that have auth issues (need attention).
    var instancesWithAuthIssues: [Instance] {
        instances.filter { instanceStatuses[$0.id] == .authFailed || instanceStatuses[$0.id] == .authRequired }
    }

    /// The currently active instance for browsing content.
    /// Falls back to the first enabled instance if no active instance is set.
    var activeInstance: Instance? {
        if let id = activeInstanceID,
           let instance = enabledInstances.first(where: { $0.id == id }) {
            return instance
        }
        return enabledInstances.first
    }

    /// Sets the given instance as the active instance for browsing.
    func setActive(_ instance: Instance) {
        guard enabledInstances.contains(where: { $0.id == instance.id }) else { return }
        activeInstanceID = instance.id
        saveActiveInstance()
        NotificationCenter.default.post(name: .activeInstanceDidChange, object: nil)
    }

    /// Clears the active instance, falling back to the first enabled instance.
    func clearActiveInstance() {
        activeInstanceID = nil
        localDefaults.removeObject(forKey: activeInstanceKey)
        NotificationCenter.default.post(name: .activeInstanceDidChange, object: nil)
    }

    // MARK: - Private Methods

    private func loadInstances() {
        // Only load from local defaults - never automatically pull from iCloud
        // User must explicitly enable iCloud sync to get iCloud data
        if let data = localDefaults.data(forKey: instancesKey),
           let decoded = try? JSONDecoder().decode([Instance].self, from: data) {
            instances = decoded
        }
    }

    private func saveInstances() {
        guard let data = try? JSONEncoder().encode(instances) else { return }

        // Always write to local storage
        localDefaults.set(data, forKey: instancesKey)

        // Only write to iCloud if instance sync is enabled
        if instanceSyncEnabled {
            ubiquitousStore.set(data, forKey: instancesKey)
            settingsManager?.updateLastSyncTime()
        }

        NotificationCenter.default.post(name: .instancesDidChange, object: nil)
    }

    private func loadActiveInstance() {
        if let idString = localDefaults.string(forKey: activeInstanceKey),
           let uuid = UUID(uuidString: idString) {
            activeInstanceID = uuid
        }
    }

    private func saveActiveInstance() {
        if let id = activeInstanceID {
            localDefaults.set(id.uuidString, forKey: activeInstanceKey)
        } else {
            localDefaults.removeObject(forKey: activeInstanceKey)
        }
    }
}

// MARK: - Notifications

extension Notification.Name {
    static let instancesDidChange = Notification.Name("stream.yattee.instancesDidChange")
    static let activeInstanceDidChange = Notification.Name("stream.yattee.activeInstanceDidChange")
}
